xref: /dokuwiki/vendor/splitbrain/lesserphp/src/Parser.php (revision a646a37b3a8a80e780de8ad5cacd58b7f3248acf)
1e6380ba3SAndreas Gohr<?php
2e6380ba3SAndreas Gohr/**
3e6380ba3SAndreas Gohr * http://leafo.net/lessphp
4e6380ba3SAndreas Gohr *
5e6380ba3SAndreas Gohr * LESS CSS compiler, adapted from http://lesscss.org
6e6380ba3SAndreas Gohr *
7e6380ba3SAndreas Gohr * Copyright 2013, Leaf Corcoran <leafot@gmail.com>
8e6380ba3SAndreas Gohr * Copyright 2016, Marcus Schwarz <github@maswaba.de>
9e6380ba3SAndreas Gohr * Licensed under MIT or GPLv3, see LICENSE
10e6380ba3SAndreas Gohr */
11e6380ba3SAndreas Gohr
12e6380ba3SAndreas Gohrnamespace LesserPHP;
13e6380ba3SAndreas Gohr
14e6380ba3SAndreas Gohruse Exception;
15e6380ba3SAndreas Gohruse LesserPHP\Utils\Util;
16e6380ba3SAndreas Gohruse stdClass;
17e6380ba3SAndreas Gohr
18e6380ba3SAndreas Gohr/**
19e6380ba3SAndreas Gohr * responsible for taking a string of LESS code and converting it into a syntax tree
20e6380ba3SAndreas Gohr */
21e6380ba3SAndreas Gohrclass Parser
22e6380ba3SAndreas Gohr{
23e6380ba3SAndreas Gohr
24e6380ba3SAndreas Gohr    public $eatWhiteDefault;
25e6380ba3SAndreas Gohr    public $sourceName;
26e6380ba3SAndreas Gohr    public $writeComments;
27e6380ba3SAndreas Gohr    public $count;
28e6380ba3SAndreas Gohr    public $line;
29e6380ba3SAndreas Gohr    public $env;
30e6380ba3SAndreas Gohr    public $buffer;
31e6380ba3SAndreas Gohr    public $seenComments;
32e6380ba3SAndreas Gohr    public $inExp;
33e6380ba3SAndreas Gohr
34e6380ba3SAndreas Gohr    protected static $nextBlockId = 0; // used to uniquely identify blocks
35e6380ba3SAndreas Gohr
36e6380ba3SAndreas Gohr    protected static $precedence = [
37e6380ba3SAndreas Gohr        '=<' => 0,
38e6380ba3SAndreas Gohr        '>=' => 0,
39e6380ba3SAndreas Gohr        '=' => 0,
40e6380ba3SAndreas Gohr        '<' => 0,
41e6380ba3SAndreas Gohr        '>' => 0,
42e6380ba3SAndreas Gohr
43e6380ba3SAndreas Gohr        '+' => 1,
44e6380ba3SAndreas Gohr        '-' => 1,
45e6380ba3SAndreas Gohr        '*' => 2,
46e6380ba3SAndreas Gohr        '/' => 2,
47e6380ba3SAndreas Gohr        '%' => 2,
48e6380ba3SAndreas Gohr    ];
49e6380ba3SAndreas Gohr
50e6380ba3SAndreas Gohr    protected static $whitePattern;
51e6380ba3SAndreas Gohr    protected static $commentMulti;
52e6380ba3SAndreas Gohr
53e6380ba3SAndreas Gohr    protected static $commentSingle = '//';
54e6380ba3SAndreas Gohr    protected static $commentMultiLeft = '/*';
55e6380ba3SAndreas Gohr    protected static $commentMultiRight = '*/';
56e6380ba3SAndreas Gohr
57e6380ba3SAndreas Gohr    // regex string to match any of the operators
58e6380ba3SAndreas Gohr    protected static $operatorString;
59e6380ba3SAndreas Gohr
60e6380ba3SAndreas Gohr    // these properties will supress division unless it's inside parenthases
61e6380ba3SAndreas Gohr    protected static $supressDivisionProps =
62e6380ba3SAndreas Gohr        ['/border-radius$/i', '/^font$/i'];
63e6380ba3SAndreas Gohr
64e6380ba3SAndreas Gohr    protected $blockDirectives = [
65e6380ba3SAndreas Gohr        'font-face',
66e6380ba3SAndreas Gohr        'keyframes',
67e6380ba3SAndreas Gohr        'page',
68e6380ba3SAndreas Gohr        '-moz-document',
69e6380ba3SAndreas Gohr        'viewport',
70e6380ba3SAndreas Gohr        '-moz-viewport',
71e6380ba3SAndreas Gohr        '-o-viewport',
72e6380ba3SAndreas Gohr        '-ms-viewport'
73e6380ba3SAndreas Gohr    ];
74e6380ba3SAndreas Gohr    protected $lineDirectives = ['charset'];
75e6380ba3SAndreas Gohr
76e6380ba3SAndreas Gohr    /**
77e6380ba3SAndreas Gohr     * if we are in parens we can be more liberal with whitespace around
78e6380ba3SAndreas Gohr     * operators because it must evaluate to a single value and thus is less
79e6380ba3SAndreas Gohr     * ambiguous.
80e6380ba3SAndreas Gohr     *
81e6380ba3SAndreas Gohr     * Consider:
82e6380ba3SAndreas Gohr     *     property1: 10 -5; // is two numbers, 10 and -5
83e6380ba3SAndreas Gohr     *     property2: (10 -5); // should evaluate to 5
84e6380ba3SAndreas Gohr     */
85e6380ba3SAndreas Gohr    protected $inParens = false;
86e6380ba3SAndreas Gohr
87e6380ba3SAndreas Gohr    // caches preg escaped literals
88e6380ba3SAndreas Gohr    protected static $literalCache = [];
89e6380ba3SAndreas Gohr
90e6380ba3SAndreas Gohr    protected $currentProperty;
91e6380ba3SAndreas Gohr
92e6380ba3SAndreas Gohr    /**
93e6380ba3SAndreas Gohr     * @param string|null $sourceName name used for error messages
94e6380ba3SAndreas Gohr     */
95e6380ba3SAndreas Gohr    public function __construct(?string $sourceName = null)
96e6380ba3SAndreas Gohr    {
97e6380ba3SAndreas Gohr        $this->eatWhiteDefault = true;
98e6380ba3SAndreas Gohr        $this->sourceName = $sourceName; // name used for error messages
99e6380ba3SAndreas Gohr
100e6380ba3SAndreas Gohr        $this->writeComments = false;
101e6380ba3SAndreas Gohr
102e6380ba3SAndreas Gohr        if (!self::$operatorString) {
103e6380ba3SAndreas Gohr            self::$operatorString =
104e6380ba3SAndreas Gohr                '(' . implode('|', array_map(
105e6380ba3SAndreas Gohr                    [Util::class, 'pregQuote'],
106e6380ba3SAndreas Gohr                    array_keys(self::$precedence)
107e6380ba3SAndreas Gohr                )) . ')';
108e6380ba3SAndreas Gohr
109e6380ba3SAndreas Gohr            $commentSingle = Util::pregQuote(self::$commentSingle);
110e6380ba3SAndreas Gohr            $commentMultiLeft = Util::pregQuote(self::$commentMultiLeft);
111e6380ba3SAndreas Gohr            $commentMultiRight = Util::pregQuote(self::$commentMultiRight);
112e6380ba3SAndreas Gohr
113e6380ba3SAndreas Gohr            self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight;
114e6380ba3SAndreas Gohr            self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais';
115e6380ba3SAndreas Gohr        }
116e6380ba3SAndreas Gohr    }
117e6380ba3SAndreas Gohr
118e6380ba3SAndreas Gohr    /**
119e6380ba3SAndreas Gohr     * @throws Exception
120e6380ba3SAndreas Gohr     */
121e6380ba3SAndreas Gohr    public function parse($buffer)
122e6380ba3SAndreas Gohr    {
123e6380ba3SAndreas Gohr        $this->count = 0;
124e6380ba3SAndreas Gohr        $this->line = 1;
125e6380ba3SAndreas Gohr
126e6380ba3SAndreas Gohr        $this->env = null; // block stack
127e6380ba3SAndreas Gohr        $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
128e6380ba3SAndreas Gohr        $this->pushSpecialBlock('root');
129e6380ba3SAndreas Gohr        $this->eatWhiteDefault = true;
130e6380ba3SAndreas Gohr        $this->seenComments = [];
131e6380ba3SAndreas Gohr
132e6380ba3SAndreas Gohr        // trim whitespace on head
133e6380ba3SAndreas Gohr        // if (preg_match('/^\s+/', $this->buffer, $m)) {
134e6380ba3SAndreas Gohr        //  $this->line += substr_count($m[0], "\n");
135e6380ba3SAndreas Gohr        //  $this->buffer = ltrim($this->buffer);
136e6380ba3SAndreas Gohr        // }
137e6380ba3SAndreas Gohr        $this->whitespace();
138e6380ba3SAndreas Gohr
139e6380ba3SAndreas Gohr        // parse the entire file
140e6380ba3SAndreas Gohr        while (false !== $this->parseChunk()) {
141e6380ba3SAndreas Gohr            // no-op
142e6380ba3SAndreas Gohr        }
143e6380ba3SAndreas Gohr
144e6380ba3SAndreas Gohr        if ($this->count != strlen($this->buffer)) {
145c13ef3baSAndreas Gohr            $this->throwError(sprintf(
146c13ef3baSAndreas Gohr                "parse error: count mismatches buffer length %d != %d",
147c13ef3baSAndreas Gohr                $this->count,
148c13ef3baSAndreas Gohr                strlen($this->buffer)
149c13ef3baSAndreas Gohr            ));
150e6380ba3SAndreas Gohr        }
151e6380ba3SAndreas Gohr
152e6380ba3SAndreas Gohr        // TODO report where the block was opened
153e6380ba3SAndreas Gohr        if (!property_exists($this->env, 'parent') || !is_null($this->env->parent)) {
154e6380ba3SAndreas Gohr            $this->throwError('parse error: unclosed block');
155e6380ba3SAndreas Gohr        }
156e6380ba3SAndreas Gohr
157e6380ba3SAndreas Gohr        return $this->env;
158e6380ba3SAndreas Gohr    }
159e6380ba3SAndreas Gohr
160e6380ba3SAndreas Gohr    /**
161e6380ba3SAndreas Gohr     * Parse a single chunk off the head of the buffer and append it to the
162e6380ba3SAndreas Gohr     * current parse environment.
163e6380ba3SAndreas Gohr     * Returns false when the buffer is empty, or when there is an error.
164e6380ba3SAndreas Gohr     *
165e6380ba3SAndreas Gohr     * This function is called repeatedly until the entire document is
166e6380ba3SAndreas Gohr     * parsed.
167e6380ba3SAndreas Gohr     *
168e6380ba3SAndreas Gohr     * This parser is most similar to a recursive descent parser. Single
169e6380ba3SAndreas Gohr     * functions represent discrete grammatical rules for the language, and
170e6380ba3SAndreas Gohr     * they are able to capture the text that represents those rules.
171e6380ba3SAndreas Gohr     *
172e6380ba3SAndreas Gohr     * Consider the function Lessc::keyword(). (all parse functions are
173e6380ba3SAndreas Gohr     * structured the same)
174e6380ba3SAndreas Gohr     *
175e6380ba3SAndreas Gohr     * The function takes a single reference argument. When calling the
176e6380ba3SAndreas Gohr     * function it will attempt to match a keyword on the head of the buffer.
177e6380ba3SAndreas Gohr     * If it is successful, it will place the keyword in the referenced
178e6380ba3SAndreas Gohr     * argument, advance the position in the buffer, and return true. If it
179e6380ba3SAndreas Gohr     * fails then it won't advance the buffer and it will return false.
180e6380ba3SAndreas Gohr     *
181e6380ba3SAndreas Gohr     * All of these parse functions are powered by Lessc::match(), which behaves
182e6380ba3SAndreas Gohr     * the same way, but takes a literal regular expression. Sometimes it is
183e6380ba3SAndreas Gohr     * more convenient to use match instead of creating a new function.
184e6380ba3SAndreas Gohr     *
185e6380ba3SAndreas Gohr     * Because of the format of the functions, to parse an entire string of
186e6380ba3SAndreas Gohr     * grammatical rules, you can chain them together using &&.
187e6380ba3SAndreas Gohr     *
188e6380ba3SAndreas Gohr     * But, if some of the rules in the chain succeed before one fails, then
189e6380ba3SAndreas Gohr     * the buffer position will be left at an invalid state. In order to
190e6380ba3SAndreas Gohr     * avoid this, Lessc::seek() is used to remember and set buffer positions.
191e6380ba3SAndreas Gohr     *
192e6380ba3SAndreas Gohr     * Before parsing a chain, use $s = $this->seek() to remember the current
193e6380ba3SAndreas Gohr     * position into $s. Then if a chain fails, use $this->seek($s) to
194e6380ba3SAndreas Gohr     * go back where we started.
195e6380ba3SAndreas Gohr     *
196e6380ba3SAndreas Gohr     * @throws Exception
197e6380ba3SAndreas Gohr     */
198e6380ba3SAndreas Gohr    protected function parseChunk()
199e6380ba3SAndreas Gohr    {
200e6380ba3SAndreas Gohr        if (empty($this->buffer)) return false;
201e6380ba3SAndreas Gohr        $s = $this->seek();
202e6380ba3SAndreas Gohr
203e6380ba3SAndreas Gohr        if ($this->whitespace()) {
204e6380ba3SAndreas Gohr            return true;
205e6380ba3SAndreas Gohr        }
206e6380ba3SAndreas Gohr
207e6380ba3SAndreas Gohr        // setting a property
208e6380ba3SAndreas Gohr        if ($this->keyword($key) && $this->assign() &&
209e6380ba3SAndreas Gohr            $this->propertyValue($value, $key) && $this->end()) {
210e6380ba3SAndreas Gohr            $this->append(['assign', $key, $value], $s);
211e6380ba3SAndreas Gohr            return true;
212e6380ba3SAndreas Gohr        } else {
213e6380ba3SAndreas Gohr            $this->seek($s);
214e6380ba3SAndreas Gohr        }
215e6380ba3SAndreas Gohr
216e6380ba3SAndreas Gohr
217e6380ba3SAndreas Gohr        // look for special css blocks
218e6380ba3SAndreas Gohr        if ($this->literal('@', false)) {
219e6380ba3SAndreas Gohr            $this->count--;
220e6380ba3SAndreas Gohr
221e6380ba3SAndreas Gohr            // media
222e6380ba3SAndreas Gohr            if ($this->literal('@media')) {
223e6380ba3SAndreas Gohr                if (($this->mediaQueryList($mediaQueries) || true)
224e6380ba3SAndreas Gohr                    && $this->literal('{')) {
225e6380ba3SAndreas Gohr                    $media = $this->pushSpecialBlock('media');
226e6380ba3SAndreas Gohr                    $media->queries = is_null($mediaQueries) ? [] : $mediaQueries;
227e6380ba3SAndreas Gohr                    return true;
228e6380ba3SAndreas Gohr                } else {
229e6380ba3SAndreas Gohr                    $this->seek($s);
230e6380ba3SAndreas Gohr                    return false;
231e6380ba3SAndreas Gohr                }
232e6380ba3SAndreas Gohr            }
233e6380ba3SAndreas Gohr
234e6380ba3SAndreas Gohr            if ($this->literal('@', false) && $this->keyword($dirName)) {
235e6380ba3SAndreas Gohr                if ($this->isDirective($dirName, $this->blockDirectives)) {
236e6380ba3SAndreas Gohr                    if (($this->openString('{', $dirValue, null, [';']) || true) &&
237e6380ba3SAndreas Gohr                        $this->literal('{')) {
238e6380ba3SAndreas Gohr                        $dir = $this->pushSpecialBlock('directive');
239e6380ba3SAndreas Gohr                        $dir->name = $dirName;
240e6380ba3SAndreas Gohr                        if (isset($dirValue)) $dir->value = $dirValue;
241e6380ba3SAndreas Gohr                        return true;
242e6380ba3SAndreas Gohr                    }
243e6380ba3SAndreas Gohr                } elseif ($this->isDirective($dirName, $this->lineDirectives)) {
244e6380ba3SAndreas Gohr                    if ($this->propertyValue($dirValue) && $this->end()) {
245e6380ba3SAndreas Gohr                        $this->append(['directive', $dirName, $dirValue]);
246e6380ba3SAndreas Gohr                        return true;
247e6380ba3SAndreas Gohr                    }
248e6380ba3SAndreas Gohr                } elseif ($this->literal(':', true)) {
249e6380ba3SAndreas Gohr                    //Ruleset Definition
250e6380ba3SAndreas Gohr                    if (($this->openString('{', $dirValue, null, [';']) || true) &&
251e6380ba3SAndreas Gohr                        $this->literal('{')) {
252e6380ba3SAndreas Gohr                        $dir = $this->pushBlock($this->fixTags(['@' . $dirName]));
253e6380ba3SAndreas Gohr                        $dir->name = $dirName;
254e6380ba3SAndreas Gohr                        if (isset($dirValue)) $dir->value = $dirValue;
255e6380ba3SAndreas Gohr                        return true;
256e6380ba3SAndreas Gohr                    }
257e6380ba3SAndreas Gohr                }
258e6380ba3SAndreas Gohr            }
259e6380ba3SAndreas Gohr
260e6380ba3SAndreas Gohr            $this->seek($s);
261e6380ba3SAndreas Gohr        }
262e6380ba3SAndreas Gohr
263e6380ba3SAndreas Gohr        // setting a variable
264e6380ba3SAndreas Gohr        if ($this->variable($var) && $this->assign() &&
265e6380ba3SAndreas Gohr            $this->propertyValue($value) && $this->end()) {
266e6380ba3SAndreas Gohr            $this->append(['assign', $var, $value], $s);
267e6380ba3SAndreas Gohr            return true;
268e6380ba3SAndreas Gohr        } else {
269e6380ba3SAndreas Gohr            $this->seek($s);
270e6380ba3SAndreas Gohr        }
271e6380ba3SAndreas Gohr
272e6380ba3SAndreas Gohr        if ($this->import($importValue)) {
273e6380ba3SAndreas Gohr            $this->append($importValue, $s);
274e6380ba3SAndreas Gohr            return true;
275e6380ba3SAndreas Gohr        }
276e6380ba3SAndreas Gohr
277e6380ba3SAndreas Gohr        // opening parametric mixin
278e6380ba3SAndreas Gohr        if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
279e6380ba3SAndreas Gohr            ($this->guards($guards) || true) &&
280e6380ba3SAndreas Gohr            $this->literal('{')) {
281e6380ba3SAndreas Gohr            $block = $this->pushBlock($this->fixTags([$tag]));
282e6380ba3SAndreas Gohr            $block->args = $args;
283e6380ba3SAndreas Gohr            $block->isVararg = $isVararg;
284e6380ba3SAndreas Gohr            if (!empty($guards)) $block->guards = $guards;
285e6380ba3SAndreas Gohr            return true;
286e6380ba3SAndreas Gohr        } else {
287e6380ba3SAndreas Gohr            $this->seek($s);
288e6380ba3SAndreas Gohr        }
289e6380ba3SAndreas Gohr
290e6380ba3SAndreas Gohr        // opening a simple block
291e6380ba3SAndreas Gohr        if ($this->tags($tags) && $this->literal('{', false)) {
292e6380ba3SAndreas Gohr            $tags = $this->fixTags($tags);
293e6380ba3SAndreas Gohr            $this->pushBlock($tags);
294e6380ba3SAndreas Gohr            return true;
295e6380ba3SAndreas Gohr        } else {
296e6380ba3SAndreas Gohr            $this->seek($s);
297e6380ba3SAndreas Gohr        }
298e6380ba3SAndreas Gohr
299e6380ba3SAndreas Gohr        // closing a block
300e6380ba3SAndreas Gohr        if ($this->literal('}', false)) {
301e6380ba3SAndreas Gohr            try {
302e6380ba3SAndreas Gohr                $block = $this->pop();
303e6380ba3SAndreas Gohr            } catch (Exception $e) {
304e6380ba3SAndreas Gohr                $this->seek($s);
305e6380ba3SAndreas Gohr                $this->throwError($e->getMessage());
306e6380ba3SAndreas Gohr            }
307e6380ba3SAndreas Gohr
308e6380ba3SAndreas Gohr            $hidden = false;
309e6380ba3SAndreas Gohr            if (is_null($block->type)) {
310e6380ba3SAndreas Gohr                $hidden = true;
311e6380ba3SAndreas Gohr                if (!isset($block->args)) {
312e6380ba3SAndreas Gohr                    foreach ($block->tags as $tag) {
313e6380ba3SAndreas Gohr                        if (!is_string($tag) || $tag[0] != Constants::MPREFIX) {
314e6380ba3SAndreas Gohr                            $hidden = false;
315e6380ba3SAndreas Gohr                            break;
316e6380ba3SAndreas Gohr                        }
317e6380ba3SAndreas Gohr                    }
318e6380ba3SAndreas Gohr                }
319e6380ba3SAndreas Gohr
320e6380ba3SAndreas Gohr                foreach ($block->tags as $tag) {
321e6380ba3SAndreas Gohr                    if (is_string($tag)) {
322e6380ba3SAndreas Gohr                        $this->env->children[$tag][] = $block;
323e6380ba3SAndreas Gohr                    }
324e6380ba3SAndreas Gohr                }
325e6380ba3SAndreas Gohr            }
326e6380ba3SAndreas Gohr
327e6380ba3SAndreas Gohr            if (!$hidden) {
328e6380ba3SAndreas Gohr                $this->append(['block', $block], $s);
329e6380ba3SAndreas Gohr            }
330e6380ba3SAndreas Gohr
331e6380ba3SAndreas Gohr            // this is done here so comments aren't bundled into he block that
332e6380ba3SAndreas Gohr            // was just closed
333e6380ba3SAndreas Gohr            $this->whitespace();
334e6380ba3SAndreas Gohr            return true;
335e6380ba3SAndreas Gohr        }
336e6380ba3SAndreas Gohr
337e6380ba3SAndreas Gohr        // mixin
338e6380ba3SAndreas Gohr        if ($this->mixinTags($tags) &&
339e6380ba3SAndreas Gohr            ($this->argumentDef($argv, $isVararg) || true) &&
340e6380ba3SAndreas Gohr            ($this->keyword($suffix) || true) && $this->end()) {
341e6380ba3SAndreas Gohr            $tags = $this->fixTags($tags);
342e6380ba3SAndreas Gohr            $this->append(['mixin', $tags, $argv, $suffix], $s);
343e6380ba3SAndreas Gohr            return true;
344e6380ba3SAndreas Gohr        } else {
345e6380ba3SAndreas Gohr            $this->seek($s);
346e6380ba3SAndreas Gohr        }
347e6380ba3SAndreas Gohr
348e6380ba3SAndreas Gohr        // spare ;
349e6380ba3SAndreas Gohr        if ($this->literal(';')) return true;
350e6380ba3SAndreas Gohr
351e6380ba3SAndreas Gohr        return false; // got nothing, throw error
352e6380ba3SAndreas Gohr    }
353e6380ba3SAndreas Gohr
354e6380ba3SAndreas Gohr    protected function isDirective($dirname, $directives)
355e6380ba3SAndreas Gohr    {
356e6380ba3SAndreas Gohr        // TODO: cache pattern in parser
357e6380ba3SAndreas Gohr        $pattern = implode(
358e6380ba3SAndreas Gohr            '|',
359e6380ba3SAndreas Gohr            array_map([Util::class, 'pregQuote'], $directives)
360e6380ba3SAndreas Gohr        );
361e6380ba3SAndreas Gohr        $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
362e6380ba3SAndreas Gohr
363e6380ba3SAndreas Gohr        return preg_match($pattern, $dirname);
364e6380ba3SAndreas Gohr    }
365e6380ba3SAndreas Gohr
366e6380ba3SAndreas Gohr    protected function fixTags($tags)
367e6380ba3SAndreas Gohr    {
368e6380ba3SAndreas Gohr        // move @ tags out of variable namespace
369e6380ba3SAndreas Gohr        foreach ($tags as &$tag) {
370e6380ba3SAndreas Gohr            if ($tag[0] == Constants::VPREFIX)
371e6380ba3SAndreas Gohr                $tag[0] = Constants::MPREFIX;
372e6380ba3SAndreas Gohr        }
373e6380ba3SAndreas Gohr        return $tags;
374e6380ba3SAndreas Gohr    }
375e6380ba3SAndreas Gohr
376e6380ba3SAndreas Gohr    // a list of expressions
377e6380ba3SAndreas Gohr    protected function expressionList(&$exps)
378e6380ba3SAndreas Gohr    {
379e6380ba3SAndreas Gohr        $values = [];
380e6380ba3SAndreas Gohr
381e6380ba3SAndreas Gohr        while ($this->expression($exp)) {
382e6380ba3SAndreas Gohr            $values[] = $exp;
383e6380ba3SAndreas Gohr        }
384e6380ba3SAndreas Gohr
385e6380ba3SAndreas Gohr        if (count($values) == 0) return false;
386e6380ba3SAndreas Gohr
387e6380ba3SAndreas Gohr        $exps = Lessc::compressList($values, ' ');
388e6380ba3SAndreas Gohr        return true;
389e6380ba3SAndreas Gohr    }
390e6380ba3SAndreas Gohr
391e6380ba3SAndreas Gohr    /**
392e6380ba3SAndreas Gohr     * Attempt to consume an expression.
393e6380ba3SAndreas Gohr     * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
394e6380ba3SAndreas Gohr     */
395e6380ba3SAndreas Gohr    protected function expression(&$out)
396e6380ba3SAndreas Gohr    {
397e6380ba3SAndreas Gohr        if ($this->value($lhs)) {
398e6380ba3SAndreas Gohr            $out = $this->expHelper($lhs, 0);
399e6380ba3SAndreas Gohr
400e6380ba3SAndreas Gohr            // look for / shorthand
401e6380ba3SAndreas Gohr            if (!empty($this->env->supressedDivision)) {
402e6380ba3SAndreas Gohr                unset($this->env->supressedDivision);
403e6380ba3SAndreas Gohr                $s = $this->seek();
404e6380ba3SAndreas Gohr                if ($this->literal('/') && $this->value($rhs)) {
405e6380ba3SAndreas Gohr                    $out = ['list', '', [$out, ['keyword', '/'], $rhs]];
406e6380ba3SAndreas Gohr                } else {
407e6380ba3SAndreas Gohr                    $this->seek($s);
408e6380ba3SAndreas Gohr                }
409e6380ba3SAndreas Gohr            }
410e6380ba3SAndreas Gohr
411e6380ba3SAndreas Gohr            return true;
412e6380ba3SAndreas Gohr        }
413e6380ba3SAndreas Gohr        return false;
414e6380ba3SAndreas Gohr    }
415e6380ba3SAndreas Gohr
416e6380ba3SAndreas Gohr    /**
417e6380ba3SAndreas Gohr     * recursively parse infix equation with $lhs at precedence $minP
418e6380ba3SAndreas Gohr     */
419e6380ba3SAndreas Gohr    protected function expHelper($lhs, $minP)
420e6380ba3SAndreas Gohr    {
421e6380ba3SAndreas Gohr        $this->inExp = true;
422e6380ba3SAndreas Gohr        $ss = $this->seek();
423e6380ba3SAndreas Gohr
424e6380ba3SAndreas Gohr        while (true) {
425e6380ba3SAndreas Gohr            $whiteBefore = isset($this->buffer[$this->count - 1]) &&
426e6380ba3SAndreas Gohr                ctype_space($this->buffer[$this->count - 1]);
427e6380ba3SAndreas Gohr
428e6380ba3SAndreas Gohr            // If there is whitespace before the operator, then we require
429e6380ba3SAndreas Gohr            // whitespace after the operator for it to be an expression
430e6380ba3SAndreas Gohr            $needWhite = $whiteBefore && !$this->inParens;
431e6380ba3SAndreas Gohr
432e6380ba3SAndreas Gohr            if (
433e6380ba3SAndreas Gohr                $this->match(self::$operatorString . ($needWhite ? '\s' : ''), $m) &&
434e6380ba3SAndreas Gohr                self::$precedence[$m[1]] >= $minP
435e6380ba3SAndreas Gohr            ) {
436e6380ba3SAndreas Gohr                if (
437e6380ba3SAndreas Gohr                    !$this->inParens && isset($this->env->currentProperty) && $m[1] == '/' &&
438e6380ba3SAndreas Gohr                    empty($this->env->supressedDivision)
439e6380ba3SAndreas Gohr                ) {
440e6380ba3SAndreas Gohr                    foreach (self::$supressDivisionProps as $pattern) {
441e6380ba3SAndreas Gohr                        if (preg_match($pattern, $this->env->currentProperty)) {
442e6380ba3SAndreas Gohr                            $this->env->supressedDivision = true;
443e6380ba3SAndreas Gohr                            break 2;
444e6380ba3SAndreas Gohr                        }
445e6380ba3SAndreas Gohr                    }
446e6380ba3SAndreas Gohr                }
447e6380ba3SAndreas Gohr
448e6380ba3SAndreas Gohr
449e6380ba3SAndreas Gohr                $whiteAfter = isset($this->buffer[$this->count - 1]) &&
450e6380ba3SAndreas Gohr                    ctype_space($this->buffer[$this->count - 1]);
451e6380ba3SAndreas Gohr
452e6380ba3SAndreas Gohr                if (!$this->value($rhs)) break;
453e6380ba3SAndreas Gohr
454e6380ba3SAndreas Gohr                // peek for next operator to see what to do with rhs
455e6380ba3SAndreas Gohr                if (
456e6380ba3SAndreas Gohr                    $this->peek(self::$operatorString, $next) &&
457e6380ba3SAndreas Gohr                    self::$precedence[$next[1]] > self::$precedence[$m[1]]
458e6380ba3SAndreas Gohr                ) {
459e6380ba3SAndreas Gohr                    $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
460e6380ba3SAndreas Gohr                }
461e6380ba3SAndreas Gohr
462e6380ba3SAndreas Gohr                $lhs = ['expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter];
463e6380ba3SAndreas Gohr                $ss = $this->seek();
464e6380ba3SAndreas Gohr
465e6380ba3SAndreas Gohr                continue;
466e6380ba3SAndreas Gohr            }
467e6380ba3SAndreas Gohr
468e6380ba3SAndreas Gohr            break;
469e6380ba3SAndreas Gohr        }
470e6380ba3SAndreas Gohr
471e6380ba3SAndreas Gohr        $this->seek($ss);
472e6380ba3SAndreas Gohr
473e6380ba3SAndreas Gohr        return $lhs;
474e6380ba3SAndreas Gohr    }
475e6380ba3SAndreas Gohr
476e6380ba3SAndreas Gohr    // consume a list of values for a property
477e6380ba3SAndreas Gohr    public function propertyValue(&$value, $keyName = null)
478e6380ba3SAndreas Gohr    {
479e6380ba3SAndreas Gohr        $values = [];
480e6380ba3SAndreas Gohr
481e6380ba3SAndreas Gohr        if ($keyName !== null) $this->env->currentProperty = $keyName;
482e6380ba3SAndreas Gohr
483e6380ba3SAndreas Gohr        $s = null;
484e6380ba3SAndreas Gohr        while ($this->expressionList($v)) {
485e6380ba3SAndreas Gohr            $values[] = $v;
486e6380ba3SAndreas Gohr            $s = $this->seek();
487e6380ba3SAndreas Gohr            if (!$this->literal(',')) break;
488e6380ba3SAndreas Gohr        }
489e6380ba3SAndreas Gohr
490e6380ba3SAndreas Gohr        if ($s) $this->seek($s);
491e6380ba3SAndreas Gohr
492e6380ba3SAndreas Gohr        if ($keyName !== null) unset($this->env->currentProperty);
493e6380ba3SAndreas Gohr
494e6380ba3SAndreas Gohr        if (count($values) == 0) return false;
495e6380ba3SAndreas Gohr
496e6380ba3SAndreas Gohr        $value = Lessc::compressList($values, ', ');
497e6380ba3SAndreas Gohr        return true;
498e6380ba3SAndreas Gohr    }
499e6380ba3SAndreas Gohr
500e6380ba3SAndreas Gohr    protected function parenValue(&$out)
501e6380ba3SAndreas Gohr    {
502e6380ba3SAndreas Gohr        $s = $this->seek();
503e6380ba3SAndreas Gohr
504e6380ba3SAndreas Gohr        // speed shortcut
505e6380ba3SAndreas Gohr        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != '(') {
506e6380ba3SAndreas Gohr            return false;
507e6380ba3SAndreas Gohr        }
508e6380ba3SAndreas Gohr
509e6380ba3SAndreas Gohr        $inParens = $this->inParens;
510e6380ba3SAndreas Gohr        if ($this->literal('(') &&
511e6380ba3SAndreas Gohr            ($this->inParens = true) && $this->expression($exp) &&
512e6380ba3SAndreas Gohr            $this->literal(')')) {
513e6380ba3SAndreas Gohr            $out = $exp;
514e6380ba3SAndreas Gohr            $this->inParens = $inParens;
515e6380ba3SAndreas Gohr            return true;
516e6380ba3SAndreas Gohr        } else {
517e6380ba3SAndreas Gohr            $this->inParens = $inParens;
518e6380ba3SAndreas Gohr            $this->seek($s);
519e6380ba3SAndreas Gohr        }
520e6380ba3SAndreas Gohr
521e6380ba3SAndreas Gohr        return false;
522e6380ba3SAndreas Gohr    }
523e6380ba3SAndreas Gohr
524e6380ba3SAndreas Gohr    // a single value
525e6380ba3SAndreas Gohr    protected function value(&$value)
526e6380ba3SAndreas Gohr    {
527e6380ba3SAndreas Gohr        $s = $this->seek();
528e6380ba3SAndreas Gohr
529e6380ba3SAndreas Gohr        // speed shortcut
530e6380ba3SAndreas Gohr        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == '-') {
531e6380ba3SAndreas Gohr            // negation
532e6380ba3SAndreas Gohr            if ($this->literal('-', false) &&
533e6380ba3SAndreas Gohr                (($this->variable($inner) && $inner = ['variable', $inner]) ||
534e6380ba3SAndreas Gohr                    $this->unit($inner) ||
535e6380ba3SAndreas Gohr                    $this->parenValue($inner))) {
536e6380ba3SAndreas Gohr                $value = ['unary', '-', $inner];
537e6380ba3SAndreas Gohr                return true;
538e6380ba3SAndreas Gohr            } else {
539e6380ba3SAndreas Gohr                $this->seek($s);
540e6380ba3SAndreas Gohr            }
541e6380ba3SAndreas Gohr        }
542e6380ba3SAndreas Gohr
543e6380ba3SAndreas Gohr        if ($this->parenValue($value)) return true;
544e6380ba3SAndreas Gohr        if ($this->unit($value)) return true;
545e6380ba3SAndreas Gohr        if ($this->color($value)) return true;
546e6380ba3SAndreas Gohr        if ($this->func($value)) return true;
547e6380ba3SAndreas Gohr        if ($this->stringValue($value)) return true;
548e6380ba3SAndreas Gohr
549e6380ba3SAndreas Gohr        if ($this->keyword($word)) {
550e6380ba3SAndreas Gohr            $value = ['keyword', $word];
551e6380ba3SAndreas Gohr            return true;
552e6380ba3SAndreas Gohr        }
553e6380ba3SAndreas Gohr
554e6380ba3SAndreas Gohr        // try a variable
555e6380ba3SAndreas Gohr        if ($this->variable($var)) {
556e6380ba3SAndreas Gohr            $value = ['variable', $var];
557e6380ba3SAndreas Gohr            return true;
558e6380ba3SAndreas Gohr        }
559e6380ba3SAndreas Gohr
560e6380ba3SAndreas Gohr        // unquote string (should this work on any type?
561e6380ba3SAndreas Gohr        if ($this->literal('~') && $this->stringValue($str)) {
562e6380ba3SAndreas Gohr            $value = ['escape', $str];
563e6380ba3SAndreas Gohr            return true;
564e6380ba3SAndreas Gohr        } else {
565e6380ba3SAndreas Gohr            $this->seek($s);
566e6380ba3SAndreas Gohr        }
567e6380ba3SAndreas Gohr
568e6380ba3SAndreas Gohr        // css hack: \0
569e6380ba3SAndreas Gohr        if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
570e6380ba3SAndreas Gohr            $value = ['keyword', '\\' . $m[1]];
571e6380ba3SAndreas Gohr            return true;
572e6380ba3SAndreas Gohr        } else {
573e6380ba3SAndreas Gohr            $this->seek($s);
574e6380ba3SAndreas Gohr        }
575e6380ba3SAndreas Gohr
576e6380ba3SAndreas Gohr        return false;
577e6380ba3SAndreas Gohr    }
578e6380ba3SAndreas Gohr
579e6380ba3SAndreas Gohr    // an import statement
580e6380ba3SAndreas Gohr    protected function import(&$out)
581e6380ba3SAndreas Gohr    {
582e6380ba3SAndreas Gohr        if (!$this->literal('@import')) return false;
583e6380ba3SAndreas Gohr
584e6380ba3SAndreas Gohr        // @import "something.css" media;
585e6380ba3SAndreas Gohr        // @import url("something.css") media;
586e6380ba3SAndreas Gohr        // @import url(something.css) media;
587e6380ba3SAndreas Gohr
588e6380ba3SAndreas Gohr        if ($this->propertyValue($value)) {
589e6380ba3SAndreas Gohr            $out = ['import', $value];
590e6380ba3SAndreas Gohr            return true;
591e6380ba3SAndreas Gohr        }
592e6380ba3SAndreas Gohr        return false;
593e6380ba3SAndreas Gohr    }
594e6380ba3SAndreas Gohr
595e6380ba3SAndreas Gohr    protected function mediaQueryList(&$out)
596e6380ba3SAndreas Gohr    {
597e6380ba3SAndreas Gohr        if ($this->genericList($list, 'mediaQuery', ',', false)) {
598e6380ba3SAndreas Gohr            $out = $list[2];
599e6380ba3SAndreas Gohr            return true;
600e6380ba3SAndreas Gohr        }
601e6380ba3SAndreas Gohr        return false;
602e6380ba3SAndreas Gohr    }
603e6380ba3SAndreas Gohr
604e6380ba3SAndreas Gohr    protected function mediaQuery(&$out)
605e6380ba3SAndreas Gohr    {
606e6380ba3SAndreas Gohr        $s = $this->seek();
607e6380ba3SAndreas Gohr
608e6380ba3SAndreas Gohr        $expressions = null;
609e6380ba3SAndreas Gohr        $parts = [];
610e6380ba3SAndreas Gohr
611e6380ba3SAndreas Gohr        if (
612e6380ba3SAndreas Gohr            (
613e6380ba3SAndreas Gohr                $this->literal('only') && ($only = true) ||
614e6380ba3SAndreas Gohr                $this->literal('not') && ($not = true) ||
615e6380ba3SAndreas Gohr                true
616e6380ba3SAndreas Gohr            ) &&
617e6380ba3SAndreas Gohr            $this->keyword($mediaType)
618e6380ba3SAndreas Gohr        ) {
619e6380ba3SAndreas Gohr            $prop = ['mediaType'];
620e6380ba3SAndreas Gohr            if (isset($only)) $prop[] = 'only';
621e6380ba3SAndreas Gohr            if (isset($not)) $prop[] = 'not';
622e6380ba3SAndreas Gohr            $prop[] = $mediaType;
623e6380ba3SAndreas Gohr            $parts[] = $prop;
624e6380ba3SAndreas Gohr        } else {
625e6380ba3SAndreas Gohr            $this->seek($s);
626e6380ba3SAndreas Gohr        }
627e6380ba3SAndreas Gohr
628e6380ba3SAndreas Gohr
629e6380ba3SAndreas Gohr        if (!empty($mediaType) && !$this->literal('and')) {
630e6380ba3SAndreas Gohr            // ~
631e6380ba3SAndreas Gohr        } else {
632e6380ba3SAndreas Gohr            $this->genericList($expressions, 'mediaExpression', 'and', false);
633e6380ba3SAndreas Gohr            if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
634e6380ba3SAndreas Gohr        }
635e6380ba3SAndreas Gohr
636e6380ba3SAndreas Gohr        if (count($parts) == 0) {
637e6380ba3SAndreas Gohr            $this->seek($s);
638e6380ba3SAndreas Gohr            return false;
639e6380ba3SAndreas Gohr        }
640e6380ba3SAndreas Gohr
641e6380ba3SAndreas Gohr        $out = $parts;
642e6380ba3SAndreas Gohr        return true;
643e6380ba3SAndreas Gohr    }
644e6380ba3SAndreas Gohr
645e6380ba3SAndreas Gohr    protected function mediaExpression(&$out)
646e6380ba3SAndreas Gohr    {
647e6380ba3SAndreas Gohr        $s = $this->seek();
648e6380ba3SAndreas Gohr        $value = null;
649e6380ba3SAndreas Gohr        if ($this->literal('(') &&
650e6380ba3SAndreas Gohr            $this->keyword($feature) &&
651e6380ba3SAndreas Gohr            ($this->literal(':') && $this->expression($value) || true) &&
652e6380ba3SAndreas Gohr            $this->literal(')')) {
653e6380ba3SAndreas Gohr            $out = ['mediaExp', $feature];
654e6380ba3SAndreas Gohr            if ($value) $out[] = $value;
655e6380ba3SAndreas Gohr            return true;
656e6380ba3SAndreas Gohr        } elseif ($this->variable($variable)) {
657e6380ba3SAndreas Gohr            $out = ['variable', $variable];
658e6380ba3SAndreas Gohr            return true;
659e6380ba3SAndreas Gohr        }
660e6380ba3SAndreas Gohr
661e6380ba3SAndreas Gohr        $this->seek($s);
662e6380ba3SAndreas Gohr        return false;
663e6380ba3SAndreas Gohr    }
664e6380ba3SAndreas Gohr
665e6380ba3SAndreas Gohr    // an unbounded string stopped by $end
666e6380ba3SAndreas Gohr    protected function openString($end, &$out, $nestingOpen = null, $rejectStrs = null)
667e6380ba3SAndreas Gohr    {
668e6380ba3SAndreas Gohr        $oldWhite = $this->eatWhiteDefault;
669e6380ba3SAndreas Gohr        $this->eatWhiteDefault = false;
670e6380ba3SAndreas Gohr
671e6380ba3SAndreas Gohr        $stop = ["'", '"', '@{', $end];
672e6380ba3SAndreas Gohr        $stop = array_map([Util::class, 'pregQuote'], $stop);
673e6380ba3SAndreas Gohr        // $stop[] = self::$commentMulti;
674e6380ba3SAndreas Gohr
675e6380ba3SAndreas Gohr        if (!is_null($rejectStrs)) {
676e6380ba3SAndreas Gohr            $stop = array_merge($stop, $rejectStrs);
677e6380ba3SAndreas Gohr        }
678e6380ba3SAndreas Gohr
679e6380ba3SAndreas Gohr        $patt = '(.*?)(' . implode('|', $stop) . ')';
680e6380ba3SAndreas Gohr
681e6380ba3SAndreas Gohr        $nestingLevel = 0;
682e6380ba3SAndreas Gohr
683e6380ba3SAndreas Gohr        $content = [];
684e6380ba3SAndreas Gohr        while ($this->match($patt, $m, false)) {
685e6380ba3SAndreas Gohr            if (!empty($m[1])) {
686e6380ba3SAndreas Gohr                $content[] = $m[1];
687e6380ba3SAndreas Gohr                if ($nestingOpen) {
688e6380ba3SAndreas Gohr                    $nestingLevel += substr_count($m[1], $nestingOpen);
689e6380ba3SAndreas Gohr                }
690e6380ba3SAndreas Gohr            }
691e6380ba3SAndreas Gohr
692e6380ba3SAndreas Gohr            $tok = $m[2];
693e6380ba3SAndreas Gohr
694e6380ba3SAndreas Gohr            $this->count -= strlen($tok);
695e6380ba3SAndreas Gohr            if ($tok == $end) {
696e6380ba3SAndreas Gohr                if ($nestingLevel == 0) {
697e6380ba3SAndreas Gohr                    break;
698e6380ba3SAndreas Gohr                } else {
699e6380ba3SAndreas Gohr                    $nestingLevel--;
700e6380ba3SAndreas Gohr                }
701e6380ba3SAndreas Gohr            }
702e6380ba3SAndreas Gohr
703e6380ba3SAndreas Gohr            if (($tok == "'" || $tok == '"') && $this->stringValue($str)) {
704e6380ba3SAndreas Gohr                $content[] = $str;
705e6380ba3SAndreas Gohr                continue;
706e6380ba3SAndreas Gohr            }
707e6380ba3SAndreas Gohr
708e6380ba3SAndreas Gohr            if ($tok == '@{' && $this->interpolation($inter)) {
709e6380ba3SAndreas Gohr                $content[] = $inter;
710e6380ba3SAndreas Gohr                continue;
711e6380ba3SAndreas Gohr            }
712e6380ba3SAndreas Gohr
713e6380ba3SAndreas Gohr            if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
714e6380ba3SAndreas Gohr                break;
715e6380ba3SAndreas Gohr            }
716e6380ba3SAndreas Gohr
717e6380ba3SAndreas Gohr            $content[] = $tok;
718e6380ba3SAndreas Gohr            $this->count += strlen($tok);
719e6380ba3SAndreas Gohr        }
720e6380ba3SAndreas Gohr
721e6380ba3SAndreas Gohr        $this->eatWhiteDefault = $oldWhite;
722e6380ba3SAndreas Gohr
723e6380ba3SAndreas Gohr        if (count($content) == 0) return false;
724e6380ba3SAndreas Gohr
725e6380ba3SAndreas Gohr        // trim the end
726e6380ba3SAndreas Gohr        if (is_string(end($content))) {
727e6380ba3SAndreas Gohr            $content[count($content) - 1] = rtrim(end($content));
728e6380ba3SAndreas Gohr        }
729e6380ba3SAndreas Gohr
730e6380ba3SAndreas Gohr        $out = ['string', '', $content];
731e6380ba3SAndreas Gohr        return true;
732e6380ba3SAndreas Gohr    }
733e6380ba3SAndreas Gohr
734e6380ba3SAndreas Gohr    protected function stringValue(&$out)
735e6380ba3SAndreas Gohr    {
736e6380ba3SAndreas Gohr        $s = $this->seek();
737e6380ba3SAndreas Gohr        if ($this->literal('"', false)) {
738e6380ba3SAndreas Gohr            $delim = '"';
739e6380ba3SAndreas Gohr        } elseif ($this->literal("'", false)) {
740e6380ba3SAndreas Gohr            $delim = "'";
741e6380ba3SAndreas Gohr        } else {
742e6380ba3SAndreas Gohr            return false;
743e6380ba3SAndreas Gohr        }
744e6380ba3SAndreas Gohr
745e6380ba3SAndreas Gohr        $content = [];
746e6380ba3SAndreas Gohr
747e6380ba3SAndreas Gohr        // look for either ending delim , escape, or string interpolation
748e6380ba3SAndreas Gohr        $patt = '([^\n]*?)(@\{|\\\\|' . Util::pregQuote($delim) . ')';
749e6380ba3SAndreas Gohr
750e6380ba3SAndreas Gohr        $oldWhite = $this->eatWhiteDefault;
751e6380ba3SAndreas Gohr        $this->eatWhiteDefault = false;
752e6380ba3SAndreas Gohr
753e6380ba3SAndreas Gohr        while ($this->match($patt, $m, false)) {
754e6380ba3SAndreas Gohr            $content[] = $m[1];
755e6380ba3SAndreas Gohr            if ($m[2] == '@{') {
756e6380ba3SAndreas Gohr                $this->count -= strlen($m[2]);
757e6380ba3SAndreas Gohr                if ($this->interpolation($inter)) {
758e6380ba3SAndreas Gohr                    $content[] = $inter;
759e6380ba3SAndreas Gohr                } else {
760e6380ba3SAndreas Gohr                    $this->count += strlen($m[2]);
761e6380ba3SAndreas Gohr                    $content[] = '@{'; // ignore it
762e6380ba3SAndreas Gohr                }
763e6380ba3SAndreas Gohr            } elseif ($m[2] == '\\') {
764e6380ba3SAndreas Gohr                $content[] = $m[2];
765e6380ba3SAndreas Gohr                if ($this->literal($delim, false)) {
766e6380ba3SAndreas Gohr                    $content[] = $delim;
767e6380ba3SAndreas Gohr                }
768e6380ba3SAndreas Gohr            } else {
769e6380ba3SAndreas Gohr                $this->count -= strlen($delim);
770e6380ba3SAndreas Gohr                break; // delim
771e6380ba3SAndreas Gohr            }
772e6380ba3SAndreas Gohr        }
773e6380ba3SAndreas Gohr
774e6380ba3SAndreas Gohr        $this->eatWhiteDefault = $oldWhite;
775e6380ba3SAndreas Gohr
776e6380ba3SAndreas Gohr        if ($this->literal($delim)) {
777e6380ba3SAndreas Gohr            $out = ['string', $delim, $content];
778e6380ba3SAndreas Gohr            return true;
779e6380ba3SAndreas Gohr        }
780e6380ba3SAndreas Gohr
781e6380ba3SAndreas Gohr        $this->seek($s);
782e6380ba3SAndreas Gohr        return false;
783e6380ba3SAndreas Gohr    }
784e6380ba3SAndreas Gohr
785e6380ba3SAndreas Gohr    protected function interpolation(&$out)
786e6380ba3SAndreas Gohr    {
787e6380ba3SAndreas Gohr        $oldWhite = $this->eatWhiteDefault;
788e6380ba3SAndreas Gohr        $this->eatWhiteDefault = true;
789e6380ba3SAndreas Gohr
790e6380ba3SAndreas Gohr        $s = $this->seek();
791e6380ba3SAndreas Gohr        if ($this->literal('@{') &&
792e6380ba3SAndreas Gohr            $this->openString('}', $interp, null, ["'", '"', ';']) &&
793e6380ba3SAndreas Gohr            $this->literal('}', false)) {
794e6380ba3SAndreas Gohr            $out = ['interpolate', $interp];
795e6380ba3SAndreas Gohr            $this->eatWhiteDefault = $oldWhite;
796e6380ba3SAndreas Gohr            if ($this->eatWhiteDefault) $this->whitespace();
797e6380ba3SAndreas Gohr            return true;
798e6380ba3SAndreas Gohr        }
799e6380ba3SAndreas Gohr
800e6380ba3SAndreas Gohr        $this->eatWhiteDefault = $oldWhite;
801e6380ba3SAndreas Gohr        $this->seek($s);
802e6380ba3SAndreas Gohr        return false;
803e6380ba3SAndreas Gohr    }
804e6380ba3SAndreas Gohr
805e6380ba3SAndreas Gohr    protected function unit(&$unit)
806e6380ba3SAndreas Gohr    {
807e6380ba3SAndreas Gohr        // speed shortcut
808e6380ba3SAndreas Gohr        if (isset($this->buffer[$this->count])) {
809e6380ba3SAndreas Gohr            $char = $this->buffer[$this->count];
810e6380ba3SAndreas Gohr            if (!ctype_digit($char) && $char != '.') return false;
811e6380ba3SAndreas Gohr        }
812e6380ba3SAndreas Gohr
813e6380ba3SAndreas Gohr        if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
814e6380ba3SAndreas Gohr            $unit = ['number', $m[1], empty($m[2]) ? '' : $m[2]];
815e6380ba3SAndreas Gohr            return true;
816e6380ba3SAndreas Gohr        }
817e6380ba3SAndreas Gohr        return false;
818e6380ba3SAndreas Gohr    }
819e6380ba3SAndreas Gohr
820e6380ba3SAndreas Gohr    // a # color
821e6380ba3SAndreas Gohr    protected function color(&$out)
822e6380ba3SAndreas Gohr    {
823e6380ba3SAndreas Gohr        if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
824e6380ba3SAndreas Gohr            if (strlen($m[1]) > 7) {
825e6380ba3SAndreas Gohr                $out = ['string', '', [$m[1]]];
826e6380ba3SAndreas Gohr            } else {
827e6380ba3SAndreas Gohr                $out = ['raw_color', $m[1]];
828e6380ba3SAndreas Gohr            }
829e6380ba3SAndreas Gohr            return true;
830e6380ba3SAndreas Gohr        }
831e6380ba3SAndreas Gohr
832e6380ba3SAndreas Gohr        return false;
833e6380ba3SAndreas Gohr    }
834e6380ba3SAndreas Gohr
835e6380ba3SAndreas Gohr    /**
836e6380ba3SAndreas Gohr     * consume an argument definition list surrounded by ()
837e6380ba3SAndreas Gohr     * each argument is a variable name with optional value
838e6380ba3SAndreas Gohr     * or at the end a ... or a variable named followed by ...
839e6380ba3SAndreas Gohr     * arguments are separated by , unless a ; is in the list, then ; is the
840e6380ba3SAndreas Gohr     * delimiter.
841e6380ba3SAndreas Gohr     *
842e6380ba3SAndreas Gohr     * @throws Exception
843e6380ba3SAndreas Gohr     */
844e6380ba3SAndreas Gohr    protected function argumentDef(&$args, &$isVararg)
845e6380ba3SAndreas Gohr    {
846e6380ba3SAndreas Gohr        $s = $this->seek();
847e6380ba3SAndreas Gohr        if (!$this->literal('(')) return false;
848e6380ba3SAndreas Gohr
849e6380ba3SAndreas Gohr        $values = [];
850e6380ba3SAndreas Gohr        $delim = ',';
851e6380ba3SAndreas Gohr        $method = 'expressionList';
852e6380ba3SAndreas Gohr
853e6380ba3SAndreas Gohr        $value = null;
854e6380ba3SAndreas Gohr        $rhs = null;
855e6380ba3SAndreas Gohr
856e6380ba3SAndreas Gohr        $isVararg = false;
857e6380ba3SAndreas Gohr        while (true) {
858e6380ba3SAndreas Gohr            if ($this->literal('...')) {
859e6380ba3SAndreas Gohr                $isVararg = true;
860e6380ba3SAndreas Gohr                break;
861e6380ba3SAndreas Gohr            }
862e6380ba3SAndreas Gohr
863e6380ba3SAndreas Gohr            if ($this->$method($value)) {
864e6380ba3SAndreas Gohr                if ($value[0] == 'variable') {
865e6380ba3SAndreas Gohr                    $arg = ['arg', $value[1]];
866e6380ba3SAndreas Gohr                    $ss = $this->seek();
867e6380ba3SAndreas Gohr
868e6380ba3SAndreas Gohr                    if ($this->assign() && $this->$method($rhs)) {
869e6380ba3SAndreas Gohr                        $arg[] = $rhs;
870e6380ba3SAndreas Gohr                    } else {
871e6380ba3SAndreas Gohr                        $this->seek($ss);
872e6380ba3SAndreas Gohr                        if ($this->literal('...')) {
873e6380ba3SAndreas Gohr                            $arg[0] = 'rest';
874e6380ba3SAndreas Gohr                            $isVararg = true;
875e6380ba3SAndreas Gohr                        }
876e6380ba3SAndreas Gohr                    }
877e6380ba3SAndreas Gohr
878e6380ba3SAndreas Gohr                    $values[] = $arg;
879e6380ba3SAndreas Gohr                    if ($isVararg) break;
880e6380ba3SAndreas Gohr                    continue;
881e6380ba3SAndreas Gohr                } else {
882e6380ba3SAndreas Gohr                    $values[] = ['lit', $value];
883e6380ba3SAndreas Gohr                }
884e6380ba3SAndreas Gohr            }
885e6380ba3SAndreas Gohr
886e6380ba3SAndreas Gohr
887e6380ba3SAndreas Gohr            if (!$this->literal($delim)) {
888e6380ba3SAndreas Gohr                if ($delim == ',' && $this->literal(';')) {
889e6380ba3SAndreas Gohr                    // found new delim, convert existing args
890e6380ba3SAndreas Gohr                    $delim = ';';
891e6380ba3SAndreas Gohr                    $method = 'propertyValue';
892e6380ba3SAndreas Gohr
893e6380ba3SAndreas Gohr                    // transform arg list
894e6380ba3SAndreas Gohr                    if (isset($values[1])) { // 2 items
895e6380ba3SAndreas Gohr                        $newList = [];
896e6380ba3SAndreas Gohr                        foreach ($values as $i => $arg) {
897e6380ba3SAndreas Gohr                            switch ($arg[0]) {
898e6380ba3SAndreas Gohr                                case 'arg':
899e6380ba3SAndreas Gohr                                    if ($i) {
900e6380ba3SAndreas Gohr                                        $this->throwError('Cannot mix ; and , as delimiter types');
901e6380ba3SAndreas Gohr                                    }
902e6380ba3SAndreas Gohr                                    $newList[] = $arg[2];
903e6380ba3SAndreas Gohr                                    break;
904e6380ba3SAndreas Gohr                                case 'lit':
905e6380ba3SAndreas Gohr                                    $newList[] = $arg[1];
906e6380ba3SAndreas Gohr                                    break;
907e6380ba3SAndreas Gohr                                case 'rest':
908e6380ba3SAndreas Gohr                                    $this->throwError('Unexpected rest before semicolon');
909e6380ba3SAndreas Gohr                            }
910e6380ba3SAndreas Gohr                        }
911e6380ba3SAndreas Gohr
912e6380ba3SAndreas Gohr                        $newList = ['list', ', ', $newList];
913e6380ba3SAndreas Gohr
914e6380ba3SAndreas Gohr                        switch ($values[0][0]) {
915e6380ba3SAndreas Gohr                            case 'arg':
916e6380ba3SAndreas Gohr                                $newArg = ['arg', $values[0][1], $newList];
917e6380ba3SAndreas Gohr                                break;
918e6380ba3SAndreas Gohr                            case 'lit':
919e6380ba3SAndreas Gohr                                $newArg = ['lit', $newList];
920e6380ba3SAndreas Gohr                                break;
921e6380ba3SAndreas Gohr                        }
922e6380ba3SAndreas Gohr                    } elseif ($values) { // 1 item
923e6380ba3SAndreas Gohr                        $newArg = $values[0];
924e6380ba3SAndreas Gohr                    }
925e6380ba3SAndreas Gohr
926e6380ba3SAndreas Gohr                    if ($newArg) {
927e6380ba3SAndreas Gohr                        $values = [$newArg];
928e6380ba3SAndreas Gohr                    }
929e6380ba3SAndreas Gohr                } else {
930e6380ba3SAndreas Gohr                    break;
931e6380ba3SAndreas Gohr                }
932e6380ba3SAndreas Gohr            }
933e6380ba3SAndreas Gohr        }
934e6380ba3SAndreas Gohr
935e6380ba3SAndreas Gohr        if (!$this->literal(')')) {
936e6380ba3SAndreas Gohr            $this->seek($s);
937e6380ba3SAndreas Gohr            return false;
938e6380ba3SAndreas Gohr        }
939e6380ba3SAndreas Gohr
940e6380ba3SAndreas Gohr        $args = $values;
941e6380ba3SAndreas Gohr
942e6380ba3SAndreas Gohr        return true;
943e6380ba3SAndreas Gohr    }
944e6380ba3SAndreas Gohr
945e6380ba3SAndreas Gohr    // consume a list of tags
946e6380ba3SAndreas Gohr    // this accepts a hanging delimiter
947e6380ba3SAndreas Gohr    protected function tags(&$tags, $simple = false, $delim = ',')
948e6380ba3SAndreas Gohr    {
949e6380ba3SAndreas Gohr        $tags = [];
950e6380ba3SAndreas Gohr        while ($this->tag($tt, $simple)) {
951e6380ba3SAndreas Gohr            $tags[] = $tt;
952e6380ba3SAndreas Gohr            if (!$this->literal($delim)) break;
953e6380ba3SAndreas Gohr        }
954e6380ba3SAndreas Gohr        if (count($tags) == 0) return false;
955e6380ba3SAndreas Gohr
956e6380ba3SAndreas Gohr        return true;
957e6380ba3SAndreas Gohr    }
958e6380ba3SAndreas Gohr
959e6380ba3SAndreas Gohr    // list of tags of specifying mixin path
960e6380ba3SAndreas Gohr    // optionally separated by > (lazy, accepts extra >)
961e6380ba3SAndreas Gohr    protected function mixinTags(&$tags)
962e6380ba3SAndreas Gohr    {
963e6380ba3SAndreas Gohr        $tags = [];
964e6380ba3SAndreas Gohr        while ($this->tag($tt, true)) {
965e6380ba3SAndreas Gohr            $tags[] = $tt;
966e6380ba3SAndreas Gohr            $this->literal('>');
967e6380ba3SAndreas Gohr        }
968e6380ba3SAndreas Gohr
969e6380ba3SAndreas Gohr        if (count($tags) == 0) return false;
970e6380ba3SAndreas Gohr
971e6380ba3SAndreas Gohr        return true;
972e6380ba3SAndreas Gohr    }
973e6380ba3SAndreas Gohr
974e6380ba3SAndreas Gohr    // a bracketed value (contained within in a tag definition)
975e6380ba3SAndreas Gohr    protected function tagBracket(&$parts, &$hasExpression)
976e6380ba3SAndreas Gohr    {
977e6380ba3SAndreas Gohr        // speed shortcut
978e6380ba3SAndreas Gohr        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != '[') {
979e6380ba3SAndreas Gohr            return false;
980e6380ba3SAndreas Gohr        }
981e6380ba3SAndreas Gohr
982e6380ba3SAndreas Gohr        $s = $this->seek();
983e6380ba3SAndreas Gohr
984e6380ba3SAndreas Gohr        $hasInterpolation = false;
985e6380ba3SAndreas Gohr
986e6380ba3SAndreas Gohr        if ($this->literal('[', false)) {
987e6380ba3SAndreas Gohr            $attrParts = ['['];
988e6380ba3SAndreas Gohr            // keyword, string, operator
989e6380ba3SAndreas Gohr            while (true) {
990e6380ba3SAndreas Gohr                if ($this->literal(']', false)) {
991e6380ba3SAndreas Gohr                    $this->count--;
992e6380ba3SAndreas Gohr                    break; // get out early
993e6380ba3SAndreas Gohr                }
994e6380ba3SAndreas Gohr
995e6380ba3SAndreas Gohr                if ($this->match('\s+', $m)) {
996e6380ba3SAndreas Gohr                    $attrParts[] = ' ';
997e6380ba3SAndreas Gohr                    continue;
998e6380ba3SAndreas Gohr                }
999e6380ba3SAndreas Gohr                if ($this->stringValue($str)) {
1000e6380ba3SAndreas Gohr                    // escape parent selector, (yuck)
1001e6380ba3SAndreas Gohr                    foreach ($str[2] as &$chunk) {
1002e6380ba3SAndreas Gohr                        if (is_string($chunk)) {
1003e6380ba3SAndreas Gohr                            $chunk = str_replace(Constants::PARENT_SELECTOR, "$&$", $chunk);
1004e6380ba3SAndreas Gohr                        }
1005e6380ba3SAndreas Gohr                    }
1006e6380ba3SAndreas Gohr
1007e6380ba3SAndreas Gohr                    $attrParts[] = $str;
1008e6380ba3SAndreas Gohr                    $hasInterpolation = true;
1009e6380ba3SAndreas Gohr                    continue;
1010e6380ba3SAndreas Gohr                }
1011e6380ba3SAndreas Gohr
1012e6380ba3SAndreas Gohr                if ($this->keyword($word)) {
1013e6380ba3SAndreas Gohr                    $attrParts[] = $word;
1014e6380ba3SAndreas Gohr                    continue;
1015e6380ba3SAndreas Gohr                }
1016e6380ba3SAndreas Gohr
1017e6380ba3SAndreas Gohr                if ($this->interpolation($inter)) {
1018e6380ba3SAndreas Gohr                    $attrParts[] = $inter;
1019e6380ba3SAndreas Gohr                    $hasInterpolation = true;
1020e6380ba3SAndreas Gohr                    continue;
1021e6380ba3SAndreas Gohr                }
1022e6380ba3SAndreas Gohr
1023e6380ba3SAndreas Gohr                // operator, handles attr namespace too
1024e6380ba3SAndreas Gohr                if ($this->match('[|-~\$\*\^=]+', $m)) {
1025e6380ba3SAndreas Gohr                    $attrParts[] = $m[0];
1026e6380ba3SAndreas Gohr                    continue;
1027e6380ba3SAndreas Gohr                }
1028e6380ba3SAndreas Gohr
1029e6380ba3SAndreas Gohr                break;
1030e6380ba3SAndreas Gohr            }
1031e6380ba3SAndreas Gohr
1032e6380ba3SAndreas Gohr            if ($this->literal(']', false)) {
1033e6380ba3SAndreas Gohr                $attrParts[] = ']';
1034e6380ba3SAndreas Gohr                foreach ($attrParts as $part) {
1035e6380ba3SAndreas Gohr                    $parts[] = $part;
1036e6380ba3SAndreas Gohr                }
1037e6380ba3SAndreas Gohr                $hasExpression = $hasExpression || $hasInterpolation;
1038e6380ba3SAndreas Gohr                return true;
1039e6380ba3SAndreas Gohr            }
1040e6380ba3SAndreas Gohr            $this->seek($s);
1041e6380ba3SAndreas Gohr        }
1042e6380ba3SAndreas Gohr
1043e6380ba3SAndreas Gohr        $this->seek($s);
1044e6380ba3SAndreas Gohr        return false;
1045e6380ba3SAndreas Gohr    }
1046e6380ba3SAndreas Gohr
1047e6380ba3SAndreas Gohr    // a space separated list of selectors
1048e6380ba3SAndreas Gohr    protected function tag(&$tag, $simple = false)
1049e6380ba3SAndreas Gohr    {
1050e6380ba3SAndreas Gohr        if ($simple)
1051e6380ba3SAndreas Gohr            $chars = '^@,:;{}\][>\(\) "\'';
1052e6380ba3SAndreas Gohr        else $chars = '^@,;{}["\'';
1053e6380ba3SAndreas Gohr
1054e6380ba3SAndreas Gohr        $s = $this->seek();
1055e6380ba3SAndreas Gohr
1056e6380ba3SAndreas Gohr        $hasExpression = false;
1057e6380ba3SAndreas Gohr        $parts = [];
1058e6380ba3SAndreas Gohr        while ($this->tagBracket($parts, $hasExpression)) {
1059e6380ba3SAndreas Gohr            // no-op
1060e6380ba3SAndreas Gohr        }
1061e6380ba3SAndreas Gohr
1062e6380ba3SAndreas Gohr        $oldWhite = $this->eatWhiteDefault;
1063e6380ba3SAndreas Gohr        $this->eatWhiteDefault = false;
1064e6380ba3SAndreas Gohr
1065e6380ba3SAndreas Gohr        while (true) {
1066e6380ba3SAndreas Gohr            if ($this->match('([' . $chars . '0-9][' . $chars . ']*)', $m)) {
1067e6380ba3SAndreas Gohr                $parts[] = $m[1];
1068e6380ba3SAndreas Gohr                if ($simple) break;
1069e6380ba3SAndreas Gohr
1070e6380ba3SAndreas Gohr                while ($this->tagBracket($parts, $hasExpression)) {
1071e6380ba3SAndreas Gohr                    // no-op
1072e6380ba3SAndreas Gohr                }
1073e6380ba3SAndreas Gohr                continue;
1074e6380ba3SAndreas Gohr            }
1075e6380ba3SAndreas Gohr
1076e6380ba3SAndreas Gohr            if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == '@') {
1077e6380ba3SAndreas Gohr                if ($this->interpolation($interp)) {
1078e6380ba3SAndreas Gohr                    $hasExpression = true;
1079e6380ba3SAndreas Gohr                    $interp[2] = true; // don't unescape
1080e6380ba3SAndreas Gohr                    $parts[] = $interp;
1081e6380ba3SAndreas Gohr                    continue;
1082e6380ba3SAndreas Gohr                }
1083e6380ba3SAndreas Gohr
1084e6380ba3SAndreas Gohr                if ($this->literal('@')) {
1085e6380ba3SAndreas Gohr                    $parts[] = '@';
1086e6380ba3SAndreas Gohr                    continue;
1087e6380ba3SAndreas Gohr                }
1088e6380ba3SAndreas Gohr            }
1089e6380ba3SAndreas Gohr
1090e6380ba3SAndreas Gohr            if ($this->unit($unit)) { // for keyframes
1091e6380ba3SAndreas Gohr                $parts[] = $unit[1];
1092e6380ba3SAndreas Gohr                $parts[] = $unit[2];
1093e6380ba3SAndreas Gohr                continue;
1094e6380ba3SAndreas Gohr            }
1095e6380ba3SAndreas Gohr
1096e6380ba3SAndreas Gohr            break;
1097e6380ba3SAndreas Gohr        }
1098e6380ba3SAndreas Gohr
1099e6380ba3SAndreas Gohr        $this->eatWhiteDefault = $oldWhite;
1100e6380ba3SAndreas Gohr        if (!$parts) {
1101e6380ba3SAndreas Gohr            $this->seek($s);
1102e6380ba3SAndreas Gohr            return false;
1103e6380ba3SAndreas Gohr        }
1104e6380ba3SAndreas Gohr
1105e6380ba3SAndreas Gohr        if ($hasExpression) {
1106e6380ba3SAndreas Gohr            $tag = ['exp', ['string', '', $parts]];
1107e6380ba3SAndreas Gohr        } else {
1108e6380ba3SAndreas Gohr            $tag = trim(implode($parts));
1109e6380ba3SAndreas Gohr        }
1110e6380ba3SAndreas Gohr
1111e6380ba3SAndreas Gohr        $this->whitespace();
1112e6380ba3SAndreas Gohr        return true;
1113e6380ba3SAndreas Gohr    }
1114e6380ba3SAndreas Gohr
1115e6380ba3SAndreas Gohr    // a css function
1116e6380ba3SAndreas Gohr    protected function func(&$func)
1117e6380ba3SAndreas Gohr    {
1118e6380ba3SAndreas Gohr        $s = $this->seek();
1119e6380ba3SAndreas Gohr
1120e6380ba3SAndreas Gohr        if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
1121e6380ba3SAndreas Gohr            $fname = $m[1];
1122e6380ba3SAndreas Gohr
1123e6380ba3SAndreas Gohr            $sPreArgs = $this->seek();
1124e6380ba3SAndreas Gohr
1125e6380ba3SAndreas Gohr            $args = [];
1126e6380ba3SAndreas Gohr            while (true) {
1127e6380ba3SAndreas Gohr                $ss = $this->seek();
1128e6380ba3SAndreas Gohr                // this ugly nonsense is for ie filter properties
1129e6380ba3SAndreas Gohr                if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
1130e6380ba3SAndreas Gohr                    $args[] = ['string', '', [$name, '=', $value]];
1131e6380ba3SAndreas Gohr                } else {
1132e6380ba3SAndreas Gohr                    $this->seek($ss);
1133e6380ba3SAndreas Gohr                    if ($this->expressionList($value)) {
1134e6380ba3SAndreas Gohr                        $args[] = $value;
1135e6380ba3SAndreas Gohr                    }
1136e6380ba3SAndreas Gohr                }
1137e6380ba3SAndreas Gohr
1138e6380ba3SAndreas Gohr                if (!$this->literal(',')) break;
1139e6380ba3SAndreas Gohr            }
1140e6380ba3SAndreas Gohr            $args = ['list', ',', $args];
1141e6380ba3SAndreas Gohr
1142e6380ba3SAndreas Gohr            if ($this->literal(')')) {
1143e6380ba3SAndreas Gohr                $func = ['function', $fname, $args];
1144e6380ba3SAndreas Gohr                return true;
1145e6380ba3SAndreas Gohr            } elseif ($fname == 'url') {
1146e6380ba3SAndreas Gohr                // couldn't parse and in url? treat as string
1147e6380ba3SAndreas Gohr                $this->seek($sPreArgs);
1148e6380ba3SAndreas Gohr                if ($this->openString(')', $string) && $this->literal(')')) {
1149e6380ba3SAndreas Gohr                    $func = ['function', $fname, $string];
1150e6380ba3SAndreas Gohr                    return true;
1151e6380ba3SAndreas Gohr                }
1152e6380ba3SAndreas Gohr            }
1153e6380ba3SAndreas Gohr        }
1154e6380ba3SAndreas Gohr
1155e6380ba3SAndreas Gohr        $this->seek($s);
1156e6380ba3SAndreas Gohr        return false;
1157e6380ba3SAndreas Gohr    }
1158e6380ba3SAndreas Gohr
1159e6380ba3SAndreas Gohr    // consume a less variable
1160e6380ba3SAndreas Gohr    protected function variable(&$name)
1161e6380ba3SAndreas Gohr    {
1162e6380ba3SAndreas Gohr        $s = $this->seek();
1163e6380ba3SAndreas Gohr        if ($this->literal(Constants::VPREFIX, false) &&
1164e6380ba3SAndreas Gohr            ($this->variable($sub) || $this->keyword($name))) {
1165e6380ba3SAndreas Gohr            if (!empty($sub)) {
1166e6380ba3SAndreas Gohr                $name = ['variable', $sub];
1167e6380ba3SAndreas Gohr            } else {
1168e6380ba3SAndreas Gohr                $name = Constants::VPREFIX . $name;
1169e6380ba3SAndreas Gohr            }
1170e6380ba3SAndreas Gohr            return true;
1171e6380ba3SAndreas Gohr        }
1172e6380ba3SAndreas Gohr
1173e6380ba3SAndreas Gohr        $name = null;
1174e6380ba3SAndreas Gohr        $this->seek($s);
1175e6380ba3SAndreas Gohr        return false;
1176e6380ba3SAndreas Gohr    }
1177e6380ba3SAndreas Gohr
1178e6380ba3SAndreas Gohr    /**
1179e6380ba3SAndreas Gohr     * Consume an assignment operator
1180e6380ba3SAndreas Gohr     * Can optionally take a name that will be set to the current property name
1181e6380ba3SAndreas Gohr     */
1182e6380ba3SAndreas Gohr    protected function assign($name = null)
1183e6380ba3SAndreas Gohr    {
1184e6380ba3SAndreas Gohr        if ($name) $this->currentProperty = $name;
1185e6380ba3SAndreas Gohr        return $this->literal(':') || $this->literal('=');
1186e6380ba3SAndreas Gohr    }
1187e6380ba3SAndreas Gohr
1188e6380ba3SAndreas Gohr    // consume a keyword
1189e6380ba3SAndreas Gohr    protected function keyword(&$word)
1190e6380ba3SAndreas Gohr    {
1191e6380ba3SAndreas Gohr        if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
1192e6380ba3SAndreas Gohr            $word = $m[1];
1193e6380ba3SAndreas Gohr            return true;
1194e6380ba3SAndreas Gohr        }
1195e6380ba3SAndreas Gohr        return false;
1196e6380ba3SAndreas Gohr    }
1197e6380ba3SAndreas Gohr
1198e6380ba3SAndreas Gohr    // consume an end of statement delimiter
1199e6380ba3SAndreas Gohr    protected function end()
1200e6380ba3SAndreas Gohr    {
1201e6380ba3SAndreas Gohr        if ($this->literal(';', false)) {
1202e6380ba3SAndreas Gohr            return true;
1203e6380ba3SAndreas Gohr        } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
1204e6380ba3SAndreas Gohr            // if there is end of file or a closing block next then we don't need a ;
1205e6380ba3SAndreas Gohr            return true;
1206e6380ba3SAndreas Gohr        }
1207e6380ba3SAndreas Gohr        return false;
1208e6380ba3SAndreas Gohr    }
1209e6380ba3SAndreas Gohr
1210e6380ba3SAndreas Gohr    protected function guards(&$guards)
1211e6380ba3SAndreas Gohr    {
1212e6380ba3SAndreas Gohr        $s = $this->seek();
1213e6380ba3SAndreas Gohr
1214e6380ba3SAndreas Gohr        if (!$this->literal('when')) {
1215e6380ba3SAndreas Gohr            $this->seek($s);
1216e6380ba3SAndreas Gohr            return false;
1217e6380ba3SAndreas Gohr        }
1218e6380ba3SAndreas Gohr
1219e6380ba3SAndreas Gohr        $guards = [];
1220e6380ba3SAndreas Gohr
1221e6380ba3SAndreas Gohr        while ($this->guardGroup($g)) {
1222e6380ba3SAndreas Gohr            $guards[] = $g;
1223e6380ba3SAndreas Gohr            if (!$this->literal(',')) break;
1224e6380ba3SAndreas Gohr        }
1225e6380ba3SAndreas Gohr
1226e6380ba3SAndreas Gohr        if (count($guards) == 0) {
1227e6380ba3SAndreas Gohr            $guards = null;
1228e6380ba3SAndreas Gohr            $this->seek($s);
1229e6380ba3SAndreas Gohr            return false;
1230e6380ba3SAndreas Gohr        }
1231e6380ba3SAndreas Gohr
1232e6380ba3SAndreas Gohr        return true;
1233e6380ba3SAndreas Gohr    }
1234e6380ba3SAndreas Gohr
1235e6380ba3SAndreas Gohr    // a bunch of guards that are and'd together
1236e6380ba3SAndreas Gohr    // TODO rename to guardGroup
1237e6380ba3SAndreas Gohr    protected function guardGroup(&$guardGroup)
1238e6380ba3SAndreas Gohr    {
1239e6380ba3SAndreas Gohr        $s = $this->seek();
1240e6380ba3SAndreas Gohr        $guardGroup = [];
1241e6380ba3SAndreas Gohr        while ($this->guard($guard)) {
1242e6380ba3SAndreas Gohr            $guardGroup[] = $guard;
1243e6380ba3SAndreas Gohr            if (!$this->literal('and')) break;
1244e6380ba3SAndreas Gohr        }
1245e6380ba3SAndreas Gohr
1246e6380ba3SAndreas Gohr        if (count($guardGroup) == 0) {
1247e6380ba3SAndreas Gohr            $guardGroup = null;
1248e6380ba3SAndreas Gohr            $this->seek($s);
1249e6380ba3SAndreas Gohr            return false;
1250e6380ba3SAndreas Gohr        }
1251e6380ba3SAndreas Gohr
1252e6380ba3SAndreas Gohr        return true;
1253e6380ba3SAndreas Gohr    }
1254e6380ba3SAndreas Gohr
1255e6380ba3SAndreas Gohr    protected function guard(&$guard)
1256e6380ba3SAndreas Gohr    {
1257e6380ba3SAndreas Gohr        $s = $this->seek();
1258e6380ba3SAndreas Gohr        $negate = $this->literal('not');
1259e6380ba3SAndreas Gohr
1260e6380ba3SAndreas Gohr        if ($this->literal('(') && $this->expression($exp) && $this->literal(')')) {
1261e6380ba3SAndreas Gohr            $guard = $exp;
1262e6380ba3SAndreas Gohr            if ($negate) $guard = ['negate', $guard];
1263e6380ba3SAndreas Gohr            return true;
1264e6380ba3SAndreas Gohr        }
1265e6380ba3SAndreas Gohr
1266e6380ba3SAndreas Gohr        $this->seek($s);
1267e6380ba3SAndreas Gohr        return false;
1268e6380ba3SAndreas Gohr    }
1269e6380ba3SAndreas Gohr
1270e6380ba3SAndreas Gohr    /* raw parsing functions */
1271e6380ba3SAndreas Gohr
1272e6380ba3SAndreas Gohr    protected function literal($what, $eatWhitespace = null)
1273e6380ba3SAndreas Gohr    {
1274e6380ba3SAndreas Gohr        if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
1275e6380ba3SAndreas Gohr
1276e6380ba3SAndreas Gohr        // shortcut on single letter
1277e6380ba3SAndreas Gohr        if (!isset($what[1]) && isset($this->buffer[$this->count])) {
1278e6380ba3SAndreas Gohr            if ($this->buffer[$this->count] == $what) {
1279e6380ba3SAndreas Gohr                if (!$eatWhitespace) {
1280e6380ba3SAndreas Gohr                    $this->count++;
1281e6380ba3SAndreas Gohr                    return true;
1282e6380ba3SAndreas Gohr                }
1283e6380ba3SAndreas Gohr                // goes below...
1284e6380ba3SAndreas Gohr            } else {
1285e6380ba3SAndreas Gohr                return false;
1286e6380ba3SAndreas Gohr            }
1287e6380ba3SAndreas Gohr        }
1288e6380ba3SAndreas Gohr
1289e6380ba3SAndreas Gohr        if (!isset(self::$literalCache[$what])) {
1290e6380ba3SAndreas Gohr            self::$literalCache[$what] = Util::pregQuote($what);
1291e6380ba3SAndreas Gohr        }
1292e6380ba3SAndreas Gohr
1293e6380ba3SAndreas Gohr        return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
1294e6380ba3SAndreas Gohr    }
1295e6380ba3SAndreas Gohr
1296e6380ba3SAndreas Gohr    protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
1297e6380ba3SAndreas Gohr    {
1298e6380ba3SAndreas Gohr        $s = $this->seek();
1299e6380ba3SAndreas Gohr        $items = [];
1300e6380ba3SAndreas Gohr        $value = null;
1301e6380ba3SAndreas Gohr        while ($this->$parseItem($value)) {
1302e6380ba3SAndreas Gohr            $items[] = $value;
1303e6380ba3SAndreas Gohr            if ($delim) {
1304e6380ba3SAndreas Gohr                if (!$this->literal($delim)) break;
1305e6380ba3SAndreas Gohr            }
1306e6380ba3SAndreas Gohr        }
1307e6380ba3SAndreas Gohr
1308e6380ba3SAndreas Gohr        if (count($items) == 0) {
1309e6380ba3SAndreas Gohr            $this->seek($s);
1310e6380ba3SAndreas Gohr            return false;
1311e6380ba3SAndreas Gohr        }
1312e6380ba3SAndreas Gohr
1313e6380ba3SAndreas Gohr        if ($flatten && count($items) == 1) {
1314e6380ba3SAndreas Gohr            $out = $items[0];
1315e6380ba3SAndreas Gohr        } else {
1316e6380ba3SAndreas Gohr            $out = ['list', $delim, $items];
1317e6380ba3SAndreas Gohr        }
1318e6380ba3SAndreas Gohr
1319e6380ba3SAndreas Gohr        return true;
1320e6380ba3SAndreas Gohr    }
1321e6380ba3SAndreas Gohr
1322e6380ba3SAndreas Gohr
1323e6380ba3SAndreas Gohr    // advance counter to next occurrence of $what
1324e6380ba3SAndreas Gohr    // $until - don't include $what in advance
1325e6380ba3SAndreas Gohr    // $allowNewline, if string, will be used as valid char set
1326e6380ba3SAndreas Gohr    protected function to($what, &$out, $until = false, $allowNewline = false)
1327e6380ba3SAndreas Gohr    {
1328e6380ba3SAndreas Gohr        if (is_string($allowNewline)) {
1329e6380ba3SAndreas Gohr            $validChars = $allowNewline;
1330e6380ba3SAndreas Gohr        } else {
1331e6380ba3SAndreas Gohr            $validChars = $allowNewline ? '.' : "[^\n]";
1332e6380ba3SAndreas Gohr        }
1333e6380ba3SAndreas Gohr        if (!$this->match('(' . $validChars . '*?)' . Lessc::preg_quote($what), $m, !$until)) return false;
1334e6380ba3SAndreas Gohr        if ($until) $this->count -= strlen($what); // give back $what
1335e6380ba3SAndreas Gohr        $out = $m[1];
1336e6380ba3SAndreas Gohr        return true;
1337e6380ba3SAndreas Gohr    }
1338e6380ba3SAndreas Gohr
1339e6380ba3SAndreas Gohr    // try to match something on head of buffer
1340e6380ba3SAndreas Gohr    protected function match($regex, &$out, $eatWhitespace = null)
1341e6380ba3SAndreas Gohr    {
1342e6380ba3SAndreas Gohr        if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
1343e6380ba3SAndreas Gohr
1344e6380ba3SAndreas Gohr        $r = '/' . $regex . ($eatWhitespace && !$this->writeComments ? '\s*' : '') . '/Ais';
1345e6380ba3SAndreas Gohr        if (preg_match($r, $this->buffer, $out, 0, $this->count)) {
1346e6380ba3SAndreas Gohr            $this->count += strlen($out[0]);
1347e6380ba3SAndreas Gohr            if ($eatWhitespace && $this->writeComments) $this->whitespace();
1348e6380ba3SAndreas Gohr            return true;
1349e6380ba3SAndreas Gohr        }
1350e6380ba3SAndreas Gohr        return false;
1351e6380ba3SAndreas Gohr    }
1352e6380ba3SAndreas Gohr
1353e6380ba3SAndreas Gohr    // match some whitespace
1354e6380ba3SAndreas Gohr    protected function whitespace()
1355e6380ba3SAndreas Gohr    {
1356e6380ba3SAndreas Gohr        if ($this->writeComments) {
1357e6380ba3SAndreas Gohr            $gotWhite = false;
1358e6380ba3SAndreas Gohr            while (preg_match(self::$whitePattern, $this->buffer, $m, 0, $this->count)) {
1359e6380ba3SAndreas Gohr                if (isset($m[1]) && empty($this->seenComments[$this->count])) {
1360e6380ba3SAndreas Gohr                    $this->append(['comment', $m[1]]);
1361e6380ba3SAndreas Gohr                    $this->seenComments[$this->count] = true;
1362e6380ba3SAndreas Gohr                }
1363e6380ba3SAndreas Gohr                $this->count += strlen($m[0]);
1364e6380ba3SAndreas Gohr                $gotWhite = true;
1365e6380ba3SAndreas Gohr            }
1366e6380ba3SAndreas Gohr            return $gotWhite;
1367e6380ba3SAndreas Gohr        } else {
1368e6380ba3SAndreas Gohr            $this->match('', $m);
1369e6380ba3SAndreas Gohr            return strlen($m[0]) > 0;
1370e6380ba3SAndreas Gohr        }
1371e6380ba3SAndreas Gohr    }
1372e6380ba3SAndreas Gohr
1373e6380ba3SAndreas Gohr    // match something without consuming it
1374e6380ba3SAndreas Gohr    protected function peek($regex, &$out = null, $from = null)
1375e6380ba3SAndreas Gohr    {
1376e6380ba3SAndreas Gohr        if (is_null($from)) $from = $this->count;
1377e6380ba3SAndreas Gohr        $r = '/' . $regex . '/Ais';
1378e6380ba3SAndreas Gohr        return preg_match($r, $this->buffer, $out, 0, $from);
1379e6380ba3SAndreas Gohr    }
1380e6380ba3SAndreas Gohr
1381e6380ba3SAndreas Gohr    // seek to a spot in the buffer or return where we are on no argument
1382e6380ba3SAndreas Gohr    protected function seek($where = null)
1383e6380ba3SAndreas Gohr    {
1384e6380ba3SAndreas Gohr        if ($where === null) return $this->count;
1385e6380ba3SAndreas Gohr        else $this->count = $where;
1386e6380ba3SAndreas Gohr        return true;
1387e6380ba3SAndreas Gohr    }
1388e6380ba3SAndreas Gohr
1389e6380ba3SAndreas Gohr    /* misc functions */
1390e6380ba3SAndreas Gohr
1391e6380ba3SAndreas Gohr    /**
1392e6380ba3SAndreas Gohr     * Throw a parser exception
1393e6380ba3SAndreas Gohr     *
1394e6380ba3SAndreas Gohr     * This function tries to use the current parsing context to provide
1395e6380ba3SAndreas Gohr     * additional info on where/why the error occurred.
1396e6380ba3SAndreas Gohr     *
1397e6380ba3SAndreas Gohr     * @param string $msg The error message to throw
1398e6380ba3SAndreas Gohr     * @param int|null $count A line number counter to use instead of the current count
1399e6380ba3SAndreas Gohr     * @param \Throwable|null $previous A previous exception to chain
1400e6380ba3SAndreas Gohr     * @throws ParserException
1401e6380ba3SAndreas Gohr     */
1402*a646a37bSAndreas Gohr    public function throwError(string $msg = 'parse error', ?int $count = null, ?\Throwable $previous = null)
1403e6380ba3SAndreas Gohr    {
1404e6380ba3SAndreas Gohr        $count = is_null($count) ? $this->count : $count;
1405e6380ba3SAndreas Gohr
1406e6380ba3SAndreas Gohr        $line = $this->line + substr_count(substr($this->buffer, 0, $count), "\n");
1407e6380ba3SAndreas Gohr
1408e6380ba3SAndreas Gohr        if ($this->peek("(.*?)(\n|$)", $m, $count)) {
1409e6380ba3SAndreas Gohr            $culprit = $m[1];
1410e6380ba3SAndreas Gohr        }
1411e6380ba3SAndreas Gohr
1412e6380ba3SAndreas Gohr        throw new ParserException(
1413e6380ba3SAndreas Gohr            $msg,
1414e6380ba3SAndreas Gohr            $culprit,
1415e6380ba3SAndreas Gohr            $this->sourceName,
1416e6380ba3SAndreas Gohr            $line,
1417e6380ba3SAndreas Gohr            $previous
1418e6380ba3SAndreas Gohr        );
1419e6380ba3SAndreas Gohr    }
1420e6380ba3SAndreas Gohr
1421e6380ba3SAndreas Gohr    protected function pushBlock($selectors = null, $type = null)
1422e6380ba3SAndreas Gohr    {
1423e6380ba3SAndreas Gohr        $b = new stdClass();
1424e6380ba3SAndreas Gohr        $b->parent = $this->env;
1425e6380ba3SAndreas Gohr
1426e6380ba3SAndreas Gohr        $b->type = $type;
1427e6380ba3SAndreas Gohr        $b->id = self::$nextBlockId++;
1428e6380ba3SAndreas Gohr
1429e6380ba3SAndreas Gohr        $b->isVararg = false; // TODO: kill me from here
1430e6380ba3SAndreas Gohr        $b->tags = $selectors;
1431e6380ba3SAndreas Gohr
1432e6380ba3SAndreas Gohr        $b->props = [];
1433e6380ba3SAndreas Gohr        $b->children = [];
1434e6380ba3SAndreas Gohr
1435e6380ba3SAndreas Gohr        // add a reference to the parser so
1436e6380ba3SAndreas Gohr        // we can access the parser to throw errors
1437e6380ba3SAndreas Gohr        // or retrieve the sourceName of this block.
1438e6380ba3SAndreas Gohr        $b->parser = $this;
1439e6380ba3SAndreas Gohr
1440e6380ba3SAndreas Gohr        // so we know the position of this block
1441e6380ba3SAndreas Gohr        $b->count = $this->count;
1442e6380ba3SAndreas Gohr
1443e6380ba3SAndreas Gohr        $this->env = $b;
1444e6380ba3SAndreas Gohr        return $b;
1445e6380ba3SAndreas Gohr    }
1446e6380ba3SAndreas Gohr
1447e6380ba3SAndreas Gohr    // push a block that doesn't multiply tags
1448e6380ba3SAndreas Gohr    protected function pushSpecialBlock($type)
1449e6380ba3SAndreas Gohr    {
1450e6380ba3SAndreas Gohr        return $this->pushBlock(null, $type);
1451e6380ba3SAndreas Gohr    }
1452e6380ba3SAndreas Gohr
1453e6380ba3SAndreas Gohr    // append a property to the current block
1454e6380ba3SAndreas Gohr    protected function append($prop, $pos = null)
1455e6380ba3SAndreas Gohr    {
1456e6380ba3SAndreas Gohr        if ($pos !== null) $prop[-1] = $pos;
1457e6380ba3SAndreas Gohr        $this->env->props[] = $prop;
1458e6380ba3SAndreas Gohr    }
1459e6380ba3SAndreas Gohr
1460e6380ba3SAndreas Gohr    // pop something off the stack
1461e6380ba3SAndreas Gohr    protected function pop()
1462e6380ba3SAndreas Gohr    {
1463e6380ba3SAndreas Gohr        $old = $this->env;
1464e6380ba3SAndreas Gohr        $this->env = $this->env->parent;
1465e6380ba3SAndreas Gohr        return $old;
1466e6380ba3SAndreas Gohr    }
1467e6380ba3SAndreas Gohr
1468e6380ba3SAndreas Gohr    // remove comments from $text
1469e6380ba3SAndreas Gohr    // todo: make it work for all functions, not just url
1470e6380ba3SAndreas Gohr    protected function removeComments($text)
1471e6380ba3SAndreas Gohr    {
1472e6380ba3SAndreas Gohr        $look = ['url(', '//', '/*', '"', "'"];
1473e6380ba3SAndreas Gohr
1474e6380ba3SAndreas Gohr        $out = '';
1475e6380ba3SAndreas Gohr        $min = null;
1476e6380ba3SAndreas Gohr        while (true) {
1477e6380ba3SAndreas Gohr            // find the next item
1478e6380ba3SAndreas Gohr            foreach ($look as $token) {
1479e6380ba3SAndreas Gohr                $pos = strpos($text, $token);
1480e6380ba3SAndreas Gohr                if ($pos !== false) {
1481e6380ba3SAndreas Gohr                    if (!isset($min) || $pos < $min[1]) $min = [$token, $pos];
1482e6380ba3SAndreas Gohr                }
1483e6380ba3SAndreas Gohr            }
1484e6380ba3SAndreas Gohr
1485e6380ba3SAndreas Gohr            if (is_null($min)) break;
1486e6380ba3SAndreas Gohr
1487e6380ba3SAndreas Gohr            $count = $min[1];
1488e6380ba3SAndreas Gohr            $skip = 0;
1489e6380ba3SAndreas Gohr            $newlines = 0;
1490e6380ba3SAndreas Gohr            switch ($min[0]) {
1491e6380ba3SAndreas Gohr                case 'url(':
1492e6380ba3SAndreas Gohr                    if (preg_match('/url\(.*?\)/', $text, $m, 0, $count))
1493e6380ba3SAndreas Gohr                        $count += strlen($m[0]) - strlen($min[0]);
1494e6380ba3SAndreas Gohr                    break;
1495e6380ba3SAndreas Gohr                case '"':
1496e6380ba3SAndreas Gohr                case "'":
1497e6380ba3SAndreas Gohr                    if (preg_match('/' . $min[0] . '.*?(?<!\\\\)' . $min[0] . '/', $text, $m, 0, $count))
1498e6380ba3SAndreas Gohr                        $count += strlen($m[0]) - 1;
1499e6380ba3SAndreas Gohr                    break;
1500e6380ba3SAndreas Gohr                case '//':
1501e6380ba3SAndreas Gohr                    $skip = strpos($text, "\n", $count);
1502e6380ba3SAndreas Gohr                    if ($skip === false) $skip = strlen($text) - $count;
1503e6380ba3SAndreas Gohr                    else $skip -= $count;
1504e6380ba3SAndreas Gohr                    break;
1505e6380ba3SAndreas Gohr                case '/*':
1506e6380ba3SAndreas Gohr                    if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
1507e6380ba3SAndreas Gohr                        $skip = strlen($m[0]);
1508e6380ba3SAndreas Gohr                        $newlines = substr_count($m[0], "\n");
1509e6380ba3SAndreas Gohr                    }
1510e6380ba3SAndreas Gohr                    break;
1511e6380ba3SAndreas Gohr            }
1512e6380ba3SAndreas Gohr
1513e6380ba3SAndreas Gohr            if ($skip == 0) $count += strlen($min[0]);
1514e6380ba3SAndreas Gohr
1515e6380ba3SAndreas Gohr            $out .= substr($text, 0, $count) . str_repeat("\n", $newlines);
1516e6380ba3SAndreas Gohr            $text = substr($text, $count + $skip);
1517e6380ba3SAndreas Gohr
1518e6380ba3SAndreas Gohr            $min = null;
1519e6380ba3SAndreas Gohr        }
1520e6380ba3SAndreas Gohr
1521e6380ba3SAndreas Gohr        return $out . $text;
1522e6380ba3SAndreas Gohr    }
1523e6380ba3SAndreas Gohr}
1524