This project does not seem to handle request data directly as such no vulnerable execution paths were found.
include
, or for example
via PHP's auto-loading mechanism.
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 | protected static $nextBlockId = 0; // used to uniquely identify blocks |
||
23 | |||
24 | protected static $precedence = [ |
||
25 | '=<' => 0, |
||
26 | '>=' => 0, |
||
27 | '=' => 0, |
||
28 | '<' => 0, |
||
29 | '>' => 0, |
||
30 | |||
31 | '+' => 1, |
||
32 | '-' => 1, |
||
33 | '*' => 2, |
||
34 | '/' => 2, |
||
35 | '%' => 2, |
||
36 | ]; |
||
37 | |||
38 | protected static $whitePattern; |
||
39 | protected static $commentMulti; |
||
40 | |||
41 | protected static $commentSingle = '//'; |
||
42 | protected static $commentMultiLeft = '/*'; |
||
43 | protected static $commentMultiRight = '*/'; |
||
44 | |||
45 | // regex string to match any of the operators |
||
46 | protected static $operatorString; |
||
47 | |||
48 | // these properties will supress division unless it's inside parenthases |
||
49 | protected static $supressDivisionProps = |
||
50 | ['/border-radius$/i', '/^font$/i']; |
||
51 | |||
52 | private $blockDirectives = [ |
||
53 | 'font-face', |
||
54 | 'keyframes', |
||
55 | 'page', |
||
56 | '-moz-document', |
||
57 | 'viewport', |
||
58 | '-moz-viewport', |
||
59 | '-o-viewport', |
||
60 | '-ms-viewport', |
||
61 | ]; |
||
62 | private $lineDirectives = ['charset']; |
||
63 | |||
64 | /** |
||
65 | * if we are in parens we can be more liberal with whitespace around |
||
66 | * operators because it must evaluate to a single value and thus is less |
||
67 | * ambiguous. |
||
68 | * |
||
69 | * Consider: |
||
70 | * property1: 10 -5; // is two numbers, 10 and -5 |
||
71 | * property2: (10 -5); // should evaluate to 5 |
||
72 | */ |
||
73 | protected $inParens = false; |
||
74 | |||
75 | // caches preg escaped literals |
||
76 | protected static $literalCache = []; |
||
77 | /** @var int */ |
||
78 | public $count; |
||
79 | /** @var int */ |
||
80 | private $line; |
||
81 | /** @var array */ |
||
82 | private $seenComments; |
||
83 | /** @var string */ |
||
84 | public $buffer; |
||
85 | |||
86 | /** @var Block|null $env Block Stack */ |
||
87 | private $env; |
||
88 | /** @var bool */ |
||
89 | private $inExp; |
||
90 | /** @var string */ |
||
91 | private $currentProperty; |
||
92 | |||
93 | /** |
||
94 | * @var bool |
||
95 | */ |
||
96 | private $writeComments = false; |
||
97 | |||
98 | /** |
||
99 | * Parser constructor. |
||
100 | * |
||
101 | * @param \LesserPhp\Compiler $lessc |
||
102 | * @param string $sourceName |
||
103 | 49 | */ |
|
104 | public function __construct(Compiler $lessc, $sourceName = null) |
||
105 | 49 | { |
|
106 | $this->eatWhiteDefault = true; |
||
0 ignored issues
–
show
|
|||
107 | 49 | // reference to less needed for vPrefix, mPrefix, and parentSelector |
|
108 | $this->lessc = $lessc; |
||
0 ignored issues
–
show
The property
lessc does not exist. Did you maybe forget to declare it?
In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code: class MyClass { }
$x = new MyClass();
$x->foo = true;
Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion: class MyClass {
public $foo;
}
$x = new MyClass();
$x->foo = true;
![]() |
|||
109 | 49 | ||
110 | $this->sourceName = $sourceName; // name used for error messages |
||
0 ignored issues
–
show
The property
sourceName does not exist. Did you maybe forget to declare it?
In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code: class MyClass { }
$x = new MyClass();
$x->foo = true;
Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion: class MyClass {
public $foo;
}
$x = new MyClass();
$x->foo = true;
![]() |
|||
111 | 49 | ||
112 | 1 | if (!self::$operatorString) { |
|
113 | 1 | self::$operatorString = |
|
114 | '(' . implode('|', array_map([Compiler::class, 'pregQuote'], array_keys(self::$precedence))) . ')'; |
||
115 | 1 | ||
116 | 1 | $commentSingle = Compiler::pregQuote(self::$commentSingle); |
|
117 | 1 | $commentMultiLeft = Compiler::pregQuote(self::$commentMultiLeft); |
|
118 | $commentMultiRight = Compiler::pregQuote(self::$commentMultiRight); |
||
119 | 1 | ||
120 | 1 | self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight; |
|
121 | self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais'; |
||
122 | 49 | } |
|
123 | } |
||
124 | |||
125 | /** |
||
126 | * @param string $buffer |
||
127 | * |
||
128 | * @return Block |
||
129 | * @throws \LesserPhp\Exception\GeneralException |
||
130 | 49 | */ |
|
131 | public function parse($buffer) |
||
132 | 49 | { |
|
133 | 49 | $this->count = 0; |
|
134 | $this->line = 1; |
||
135 | 49 | ||
136 | 49 | $this->clearBlockStack(); |
|
137 | 49 | $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer); |
|
138 | 49 | $this->pushSpecialBlock('root'); |
|
139 | 49 | $this->eatWhiteDefault = true; |
|
140 | $this->seenComments = []; |
||
141 | 49 | ||
142 | $this->whitespace(); |
||
143 | |||
144 | 49 | // parse the entire file |
|
145 | while (false !== $this->parseChunk()) { |
||
146 | ; |
||
147 | } |
||
148 | 49 | ||
149 | if ($this->count !== strlen($this->buffer)) { |
||
150 | // var_dump($this->count); |
||
151 | // var_dump($this->buffer); |
||
152 | $this->throwError(); |
||
153 | } |
||
154 | |||
155 | 49 | // TODO report where the block was opened |
|
156 | if (!property_exists($this->env, 'parent') || $this->env->parent !== null) { |
||
157 | throw new GeneralException('parse error: unclosed block'); |
||
158 | } |
||
159 | 49 | ||
160 | return $this->env; |
||
161 | } |
||
162 | |||
163 | /** |
||
164 | * Parse a single chunk off the head of the buffer and append it to the |
||
165 | * current parse environment. |
||
166 | * Returns false when the buffer is empty, or when there is an error. |
||
167 | * |
||
168 | * This function is called repeatedly until the entire document is |
||
169 | * parsed. |
||
170 | * |
||
171 | * This parser is most similar to a recursive descent parser. Single |
||
172 | * functions represent discrete grammatical rules for the language, and |
||
173 | * they are able to capture the text that represents those rules. |
||
174 | * |
||
175 | * Consider the function \LesserPhp\Compiler::keyword(). (all parse functions are |
||
176 | * structured the same) |
||
177 | * |
||
178 | * The function takes a single reference argument. When calling the |
||
179 | * function it will attempt to match a keyword on the head of the buffer. |
||
180 | * If it is successful, it will place the keyword in the referenced |
||
181 | * argument, advance the position in the buffer, and return true. If it |
||
182 | * fails then it won't advance the buffer and it will return false. |
||
183 | * |
||
184 | * All of these parse functions are powered by \LesserPhp\Compiler::match(), which behaves |
||
185 | * the same way, but takes a literal regular expression. Sometimes it is |
||
186 | * more convenient to use match instead of creating a new function. |
||
187 | * |
||
188 | * Because of the format of the functions, to parse an entire string of |
||
189 | * grammatical rules, you can chain them together using &&. |
||
190 | * |
||
191 | * But, if some of the rules in the chain succeed before one fails, then |
||
192 | * the buffer position will be left at an invalid state. In order to |
||
193 | * avoid this, \LesserPhp\Compiler::seek() is used to remember and set buffer positions. |
||
194 | * |
||
195 | * Before parsing a chain, use $s = $this->seek() to remember the current |
||
196 | * position into $s. Then if a chain fails, use $this->seek($s) to |
||
197 | * go back where we started. |
||
198 | * @throws \LesserPhp\Exception\GeneralException |
||
199 | 49 | */ |
|
200 | protected function parseChunk() |
||
201 | 49 | { |
|
202 | if (empty($this->buffer)) { |
||
203 | return false; |
||
204 | 49 | } |
|
205 | $s = $this->seek(); |
||
206 | 49 | ||
207 | 46 | if ($this->whitespace()) { |
|
208 | return true; |
||
209 | } |
||
210 | |||
211 | 49 | // setting a property |
|
212 | 47 | if ($this->keyword($key) && $this->assign() && $this->propertyValue($value, $key) && $this->end()) { |
|
213 | $this->append(['assign', $key, $value], $s); |
||
214 | 47 | ||
215 | return true; |
||
216 | 49 | } else { |
|
217 | $this->seek($s); |
||
218 | } |
||
219 | |||
220 | 49 | // look for special css blocks |
|
221 | 26 | if ($this->literal('@', false)) { |
|
222 | $this->count--; |
||
223 | |||
224 | 26 | // media |
|
225 | 3 | if ($this->literal('@media')) { |
|
226 | return $this->handleLiteralMedia($s); |
||
227 | } |
||
228 | 26 | ||
229 | 26 | if ($this->literal('@', false) && $this->keyword($directiveName)) { |
|
230 | 4 | if ($this->isDirective($directiveName, $this->blockDirectives)) { |
|
231 | 4 | if ($this->handleDirectiveBlock($directiveName) === true) { |
|
232 | return true; |
||
233 | 25 | } |
|
234 | 1 | } elseif ($this->isDirective($directiveName, $this->lineDirectives)) { |
|
235 | 1 | if ($this->handleDirectiveLine($directiveName) === true) { |
|
236 | return true; |
||
237 | 25 | } |
|
238 | 24 | } elseif ($this->literal(':', true)) { |
|
239 | 1 | if ($this->handleRulesetDefinition($directiveName) === true) { |
|
240 | return true; |
||
241 | } |
||
242 | } |
||
243 | } |
||
244 | 25 | ||
245 | $this->seek($s); |
||
246 | } |
||
247 | 49 | ||
248 | 4 | if ($this->literal('&', false)) { |
|
249 | 4 | $this->count--; |
|
250 | if ($this->literal('&:extend')) { |
||
251 | // hierauf folgt was in runden klammern, und zwar das element, das erweitert werden soll |
||
252 | // heißt also, das was in klammern steht wird um die aktuellen klassen erweitert |
||
253 | /* |
||
254 | Aus |
||
255 | |||
256 | nav ul { |
||
257 | &:extend(.inline); |
||
258 | background: blue; |
||
259 | } |
||
260 | .inline { |
||
261 | color: red; |
||
262 | } |
||
263 | |||
264 | |||
265 | Wird: |
||
266 | |||
267 | nav ul { |
||
268 | background: blue; |
||
269 | } |
||
270 | .inline, |
||
271 | nav ul { |
||
272 | color: red; |
||
273 | } |
||
274 | |||
275 | */ |
||
276 | // echo "Here we go"; |
||
277 | } |
||
278 | } |
||
279 | |||
280 | |||
281 | 49 | // setting a variable |
|
282 | 49 | if ($this->variable($var) && $this->assign() && |
|
283 | $this->propertyValue($value) && $this->end() |
||
284 | 23 | ) { |
|
285 | $this->append(['assign', $var, $value], $s); |
||
286 | 23 | ||
287 | return true; |
||
288 | 49 | } else { |
|
289 | $this->seek($s); |
||
290 | } |
||
291 | 49 | ||
292 | 3 | if ($this->import($importValue)) { |
|
293 | $this->append($importValue, $s); |
||
294 | 3 | ||
295 | return true; |
||
296 | } |
||
297 | |||
298 | 49 | // opening parametric mixin |
|
299 | 49 | if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) && |
|
300 | 49 | ($this->guards($guards) || true) && |
|
301 | $this->literal('{') |
||
302 | 18 | ) { |
|
303 | 18 | $block = $this->pushBlock($this->fixTags([$tag])); |
|
304 | 18 | $block->args = $args; |
|
0 ignored issues
–
show
The property
args does not seem to exist in LesserPhp\Block .
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name. If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading. ![]() |
|||
305 | 18 | $block->isVararg = $isVararg; |
|
306 | 5 | if (!empty($guards)) { |
|
307 | $block->guards = $guards; |
||
0 ignored issues
–
show
The property
guards does not seem to exist in LesserPhp\Block .
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name. If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading. ![]() |
|||
308 | } |
||
309 | 18 | ||
310 | return true; |
||
311 | 49 | } else { |
|
312 | $this->seek($s); |
||
313 | } |
||
314 | |||
315 | 49 | // opening a simple block |
|
316 | 46 | if ($this->tags($tags) && $this->literal('{', false)) { |
|
317 | 46 | $tags = $this->fixTags($tags); |
|
318 | $this->pushBlock($tags); |
||
319 | 46 | ||
320 | return true; |
||
321 | 49 | } else { |
|
322 | $this->seek($s); |
||
323 | } |
||
324 | |||
325 | 49 | // closing a block |
|
326 | if ($this->literal('}', false)) { |
||
327 | 46 | try { |
|
328 | $block = $this->pop(); |
||
329 | } catch (\Exception $e) { |
||
330 | $this->seek($s); |
||
331 | $this->throwError($e->getMessage()); |
||
332 | |||
333 | 46 | return false; // will never be reached, but silences the ide for now |
|
334 | 46 | } |
|
335 | 46 | ||
336 | 46 | $hidden = false; |
|
337 | 46 | if ($block->type === null) { |
|
338 | 46 | $hidden = true; |
|
339 | 46 | if (!isset($block->args)) { |
|
340 | 46 | foreach ($block->tags as $tag) { |
|
0 ignored issues
–
show
The expression
$block->tags of type array|null is not guaranteed to be traversable. How about adding an additional type check?
There are different options of fixing this problem.
![]() |
|||
341 | if (!is_string($tag) || $tag[0] !== $this->lessc->getMPrefix()) { |
||
342 | $hidden = false; |
||
343 | break; |
||
344 | } |
||
345 | 46 | } |
|
346 | 46 | } |
|
347 | 46 | ||
348 | foreach ($block->tags as $tag) { |
||
0 ignored issues
–
show
The expression
$block->tags of type array|null is not guaranteed to be traversable. How about adding an additional type check?
There are different options of fixing this problem.
![]() |
|||
349 | if (is_string($tag)) { |
||
350 | $this->env->children[$tag][] = $block; |
||
351 | } |
||
352 | 46 | } |
|
353 | 46 | } |
|
354 | |||
355 | if (!$hidden) { |
||
356 | $this->append(['block', $block], $s); |
||
357 | } |
||
358 | 46 | ||
359 | // this is done here so comments aren't bundled into he block that |
||
360 | 46 | // was just closed |
|
361 | $this->whitespace(); |
||
362 | |||
363 | return true; |
||
364 | 49 | } |
|
365 | 49 | ||
366 | 49 | // mixin |
|
367 | if ($this->mixinTags($tags) && |
||
368 | 22 | ($this->argumentDef($argv, $isVararg) || true) && |
|
369 | 22 | ($this->keyword($suffix) || true) && $this->end() |
|
370 | ) { |
||
371 | 22 | $tags = $this->fixTags($tags); |
|
372 | $this->append(['mixin', $tags, $argv, $suffix], $s); |
||
373 | 49 | ||
374 | return true; |
||
375 | } else { |
||
376 | $this->seek($s); |
||
377 | 49 | } |
|
378 | 4 | ||
379 | // spare ; |
||
380 | if ($this->literal(';')) { |
||
381 | 49 | return true; |
|
382 | } |
||
383 | |||
384 | return false; // got nothing, throw error |
||
385 | } |
||
386 | |||
387 | /** |
||
388 | * @param string $directiveName |
||
389 | * @param array $directives |
||
390 | 26 | * |
|
391 | * @return bool |
||
392 | */ |
||
393 | 26 | protected function isDirective($directiveName, array $directives) |
|
394 | 26 | { |
|
395 | // TODO: cache pattern in parser |
||
396 | 26 | $pattern = implode('|', array_map([Compiler::class, 'pregQuote'], $directives)); |
|
397 | $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i'; |
||
398 | |||
399 | return (preg_match($pattern, $directiveName) === 1); |
||
400 | } |
||
401 | |||
402 | /** |
||
403 | * @param array $tags |
||
404 | 46 | * |
|
405 | * @return array |
||
406 | */ |
||
407 | 46 | protected function fixTags(array $tags) |
|
408 | 46 | { |
|
409 | 46 | // move @ tags out of variable namespace |
|
410 | foreach ($tags as &$tag) { |
||
411 | if ($tag[0] === $this->lessc->getVPrefix()) { |
||
412 | $tag[0] = $this->lessc->getMPrefix(); |
||
413 | 46 | } |
|
414 | } |
||
415 | |||
416 | return $tags; |
||
417 | } |
||
418 | |||
419 | /** |
||
420 | * a list of expressions |
||
421 | * |
||
422 | * @param $exps |
||
423 | 48 | * |
|
424 | * @return bool |
||
425 | 48 | */ |
|
426 | protected function expressionList(&$exps) |
||
427 | 48 | { |
|
428 | 48 | $values = []; |
|
429 | |||
430 | while ($this->expression($exp)) { |
||
431 | 48 | $values[] = $exp; |
|
432 | 26 | } |
|
433 | |||
434 | if (count($values) === 0) { |
||
435 | 48 | return false; |
|
436 | } |
||
437 | 48 | ||
438 | $exps = Compiler::compressList($values, ' '); |
||
439 | |||
440 | return true; |
||
441 | } |
||
442 | |||
443 | /** |
||
444 | * Attempt to consume an expression. |
||
445 | * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code |
||
446 | * |
||
447 | * @param $out |
||
448 | 48 | * |
|
449 | * @return bool |
||
450 | 48 | */ |
|
451 | 48 | protected function expression(&$out) |
|
452 | { |
||
453 | if ($this->value($lhs)) { |
||
454 | 48 | $out = $this->expHelper($lhs, 0); |
|
455 | 2 | ||
456 | 2 | // look for / shorthand |
|
457 | 2 | if (!empty($this->env->supressedDivision)) { |
|
0 ignored issues
–
show
The property
supressedDivision does not seem to exist in LesserPhp\Block .
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name. If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading. ![]() |
|||
458 | unset($this->env->supressedDivision); |
||
459 | 2 | $s = $this->seek(); |
|
460 | 2 | if ($this->literal('/') && $this->value($rhs)) { |
|
461 | 2 | $out = [ |
|
462 | 'list', |
||
463 | '', |
||
464 | [$out, ['keyword', '/'], $rhs], |
||
465 | ]; |
||
466 | } else { |
||
467 | $this->seek($s); |
||
468 | 48 | } |
|
469 | } |
||
470 | |||
471 | 48 | return true; |
|
472 | } |
||
473 | |||
474 | return false; |
||
475 | } |
||
476 | |||
477 | /** |
||
478 | * recursively parse infix equation with $lhs at precedence $minP |
||
479 | * |
||
480 | * @param $lhs |
||
481 | * @param int $minP |
||
482 | 48 | * |
|
483 | * @return array |
||
484 | 48 | */ |
|
485 | 48 | protected function expHelper($lhs, $minP) |
|
486 | { |
||
487 | 48 | $this->inExp = true; |
|
488 | 48 | $ss = $this->seek(); |
|
489 | |||
490 | while (true) { |
||
491 | $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]); |
||
492 | 48 | ||
493 | // If there is whitespace before the operator, then we require |
||
494 | 48 | // whitespace after the operator for it to be an expression |
|
495 | 48 | $needWhite = $whiteBefore && !$this->inParens; |
|
496 | |||
497 | 20 | if ($this->match(self::$operatorString . ($needWhite ? '\s' : ''), $m) && |
|
498 | 20 | self::$precedence[$m[1]] >= $minP |
|
499 | 20 | ) { |
|
500 | 20 | if (!$this->inParens && |
|
501 | isset($this->env->currentProperty) && |
||
502 | 5 | $m[1] === '/' && |
|
503 | 5 | empty($this->env->supressedDivision) |
|
504 | 2 | ) { |
|
505 | 5 | foreach (self::$supressDivisionProps as $pattern) { |
|
506 | if (preg_match($pattern, $this->env->currentProperty)) { |
||
0 ignored issues
–
show
The property
currentProperty does not seem to exist in LesserPhp\Block .
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name. If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading. ![]() |
|||
507 | $this->env->supressedDivision = true; |
||
0 ignored issues
–
show
The property
supressedDivision does not seem to exist in LesserPhp\Block .
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name. If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading. ![]() |
|||
508 | break 2; |
||
509 | } |
||
510 | 20 | } |
|
511 | } |
||
512 | 20 | ||
513 | $whiteAfter = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]); |
||
514 | |||
515 | if (!$this->value($rhs)) { |
||
516 | break; |
||
517 | 20 | } |
|
518 | 20 | ||
519 | // peek for next operator to see what to do with rhs |
||
520 | 1 | if ($this->peek(self::$operatorString, $next) && |
|
521 | self::$precedence[$next[1]] > self::$precedence[$m[1]] |
||
522 | ) { |
||
523 | 20 | $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]); |
|
524 | 20 | } |
|
525 | |||
526 | 20 | $lhs = ['expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter]; |
|
527 | $ss = $this->seek(); |
||
528 | |||
529 | 48 | continue; |
|
530 | } |
||
531 | |||
532 | 48 | break; |
|
533 | } |
||
534 | 48 | ||
535 | $this->seek($ss); |
||
536 | |||
537 | return $lhs; |
||
538 | } |
||
539 | |||
540 | /** |
||
541 | * consume a list of values for a property |
||
542 | * |
||
543 | * @param $value |
||
544 | * @param string $keyName |
||
545 | 48 | * |
|
546 | * @return bool |
||
547 | 48 | */ |
|
548 | public function propertyValue(&$value, $keyName = null) |
||
549 | 48 | { |
|
550 | 47 | $values = []; |
|
551 | |||
552 | if ($keyName !== null) { |
||
553 | 48 | $this->env->currentProperty = $keyName; |
|
0 ignored issues
–
show
The property
currentProperty does not seem to exist in LesserPhp\Block .
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name. If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading. ![]() |
|||
554 | 48 | } |
|
555 | 48 | ||
556 | 48 | $s = null; |
|
557 | 48 | while ($this->expressionList($v)) { |
|
558 | 48 | $values[] = $v; |
|
559 | $s = $this->seek(); |
||
560 | if (!$this->literal(',')) { |
||
561 | break; |
||
562 | 48 | } |
|
563 | 48 | } |
|
564 | |||
565 | if ($s) { |
||
0 ignored issues
–
show
The expression
$s of type null|integer is loosely compared to true ; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.
In PHP, under loose comparison (like For 0 == false // true
0 == null // true
123 == false // false
123 == null // false
// It is often better to use strict comparison
0 === false // false
0 === null // false
![]() |
|||
566 | 48 | $this->seek($s); |
|
567 | 47 | } |
|
568 | |||
569 | if ($keyName !== null) { |
||
570 | 48 | unset($this->env->currentProperty); |
|
571 | 3 | } |
|
572 | |||
573 | if (count($values) === 0) { |
||
574 | 48 | return false; |
|
575 | } |
||
576 | 48 | ||
577 | $value = Compiler::compressList($values, ', '); |
||
578 | |||
579 | return true; |
||
580 | } |
||
581 | |||
582 | /** |
||
583 | * @param $out |
||
584 | 48 | * |
|
585 | * @return bool |
||
586 | 48 | */ |
|
587 | protected function parenValue(&$out) |
||
588 | { |
||
589 | 48 | $s = $this->seek(); |
|
590 | 48 | ||
591 | // speed shortcut |
||
592 | if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== '(') { |
||
593 | 5 | return false; |
|
594 | 5 | } |
|
595 | 5 | ||
596 | 5 | $inParens = $this->inParens; |
|
597 | if ($this->literal('(') && |
||
598 | 3 | ($this->inParens = true) && $this->expression($exp) && |
|
599 | 3 | $this->literal(')') |
|
600 | ) { |
||
601 | 3 | $out = $exp; |
|
602 | $this->inParens = $inParens; |
||
603 | 2 | ||
604 | 2 | return true; |
|
605 | } else { |
||
606 | $this->inParens = $inParens; |
||
607 | 2 | $this->seek($s); |
|
608 | } |
||
609 | |||
610 | return false; |
||
611 | } |
||
612 | |||
613 | /** |
||
614 | * a single value |
||
615 | * |
||
616 | * @param array $value |
||
617 | 48 | * |
|
618 | * @return bool |
||
619 | 48 | */ |
|
620 | protected function value(&$value) |
||
621 | { |
||
622 | 48 | $s = $this->seek(); |
|
623 | |||
624 | 7 | // speed shortcut |
|
625 | 7 | if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '-') { |
|
626 | 6 | // negation |
|
627 | 7 | if ($this->literal('-', false) && |
|
628 | (($this->variable($inner) && $inner = ['variable', $inner]) || |
||
629 | 6 | $this->unit($inner) || |
|
630 | $this->parenValue($inner)) |
||
631 | 6 | ) { |
|
632 | $value = ['unary', '-', $inner]; |
||
633 | 2 | ||
634 | return true; |
||
635 | } else { |
||
636 | $this->seek($s); |
||
637 | 48 | } |
|
638 | 3 | } |
|
639 | |||
640 | 48 | if ($this->parenValue($value)) { |
|
641 | 38 | return true; |
|
642 | } |
||
643 | 48 | if ($this->unit($value)) { |
|
644 | 13 | return true; |
|
645 | } |
||
646 | 48 | if ($this->color($value)) { |
|
647 | 25 | return true; |
|
648 | } |
||
649 | 48 | if ($this->func($value)) { |
|
650 | 21 | return true; |
|
651 | } |
||
652 | if ($this->stringValue($value)) { |
||
653 | 48 | return true; |
|
654 | 35 | } |
|
655 | |||
656 | 35 | if ($this->keyword($word)) { |
|
657 | $value = ['keyword', $word]; |
||
658 | |||
659 | return true; |
||
660 | 48 | } |
|
661 | 29 | ||
662 | // try a variable |
||
663 | 29 | if ($this->variable($var)) { |
|
664 | $value = ['variable', $var]; |
||
665 | |||
666 | return true; |
||
667 | 48 | } |
|
668 | 4 | ||
669 | // unquote string (should this work on any type? |
||
670 | 4 | if ($this->literal('~') && $this->stringValue($str)) { |
|
671 | $value = ['escape', $str]; |
||
672 | 48 | ||
673 | return true; |
||
674 | } else { |
||
675 | $this->seek($s); |
||
676 | 48 | } |
|
677 | 1 | ||
678 | // css hack: \0 |
||
679 | 1 | if ($this->literal('\\') && $this->match('([0-9]+)', $m)) { |
|
680 | $value = ['keyword', '\\' . $m[1]]; |
||
681 | 48 | ||
682 | return true; |
||
683 | } else { |
||
684 | 48 | $this->seek($s); |
|
685 | } |
||
686 | |||
687 | return false; |
||
688 | } |
||
689 | |||
690 | /** |
||
691 | * an import statement |
||
692 | * |
||
693 | * @param array $out |
||
694 | 49 | * |
|
695 | * @return bool|null |
||
696 | 49 | */ |
|
697 | 49 | protected function import(&$out) |
|
698 | { |
||
699 | if (!$this->literal('@import')) { |
||
700 | return false; |
||
701 | } |
||
702 | |||
703 | // @import "something.css" media; |
||
704 | 3 | // @import url("something.css") media; |
|
705 | 3 | // @import url(something.css) media; |
|
706 | |||
707 | 3 | if ($this->propertyValue($value)) { |
|
708 | $out = ['import', $value]; |
||
709 | |||
710 | return true; |
||
711 | } |
||
712 | |||
713 | return false; |
||
714 | } |
||
715 | |||
716 | /** |
||
717 | * @param $out |
||
718 | 3 | * |
|
719 | * @return bool |
||
720 | 3 | */ |
|
721 | 3 | protected function mediaQueryList(&$out) |
|
722 | { |
||
723 | 3 | if ($this->genericList($list, 'mediaQuery', ',', false)) { |
|
724 | $out = $list[2]; |
||
725 | |||
726 | return true; |
||
727 | } |
||
728 | |||
729 | return false; |
||
730 | } |
||
731 | |||
732 | /** |
||
733 | * @param $out |
||
734 | 3 | * |
|
735 | * @return bool |
||
736 | 3 | */ |
|
737 | protected function mediaQuery(&$out) |
||
738 | 3 | { |
|
739 | 3 | $s = $this->seek(); |
|
740 | |||
741 | 3 | $expressions = null; |
|
742 | 3 | $parts = []; |
|
743 | |||
744 | 3 | if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) && |
|
745 | 3 | $this->keyword($mediaType) |
|
746 | ) { |
||
747 | $prop = ['mediaType']; |
||
748 | 3 | if (isset($only)) { |
|
749 | $prop[] = 'only'; |
||
750 | } |
||
751 | 3 | if (isset($not)) { |
|
752 | 3 | $prop[] = 'not'; |
|
753 | } |
||
754 | 1 | $prop[] = $mediaType; |
|
755 | $parts[] = $prop; |
||
756 | } else { |
||
757 | $this->seek($s); |
||
758 | 3 | } |
|
759 | |||
760 | |||
761 | 1 | if (!empty($mediaType) && !$this->literal('and')) { |
|
762 | 1 | // ~ |
|
763 | 1 | } else { |
|
764 | $this->genericList($expressions, 'mediaExpression', 'and', false); |
||
765 | if (is_array($expressions)) { |
||
766 | $parts = array_merge($parts, $expressions[2]); |
||
767 | 3 | } |
|
768 | } |
||
769 | |||
770 | if (count($parts) === 0) { |
||
771 | $this->seek($s); |
||
772 | |||
773 | 3 | return false; |
|
774 | } |
||
775 | 3 | ||
776 | $out = $parts; |
||
777 | |||
778 | return true; |
||
779 | } |
||
780 | |||
781 | /** |
||
782 | * @param $out |
||
783 | 1 | * |
|
784 | * @return bool |
||
785 | 1 | */ |
|
786 | 1 | protected function mediaExpression(&$out) |
|
787 | 1 | { |
|
788 | 1 | $s = $this->seek(); |
|
789 | 1 | $value = null; |
|
790 | 1 | if ($this->literal('(') && |
|
791 | $this->keyword($feature) && |
||
792 | 1 | ($this->literal(':') && $this->expression($value) || true) && |
|
793 | 1 | $this->literal(')') |
|
794 | 1 | ) { |
|
795 | $out = ['mediaExp', $feature]; |
||
796 | if ($value) { |
||
797 | 1 | $out[] = $value; |
|
798 | 1 | } |
|
799 | 1 | ||
800 | return true; |
||
801 | 1 | } elseif ($this->variable($variable)) { |
|
802 | $out = ['variable', $variable]; |
||
803 | |||
804 | return true; |
||
805 | } |
||
806 | |||
807 | $this->seek($s); |
||
808 | |||
809 | return false; |
||
810 | } |
||
811 | |||
812 | /** |
||
813 | * an unbounded string stopped by $end |
||
814 | * |
||
815 | * @param string $end |
||
816 | * @param $out |
||
817 | * @param null $nestingOpen |
||
818 | * @param string[] $rejectStrs |
||
819 | 26 | * |
|
820 | * @return bool |
||
821 | 26 | */ |
|
822 | 26 | protected function openString($end, &$out, $nestingOpen = null, $rejectStrs = null) |
|
823 | { |
||
824 | 26 | $oldWhite = $this->eatWhiteDefault; |
|
825 | 26 | $this->eatWhiteDefault = false; |
|
826 | |||
827 | $stop = ["'", '"', '@{', $end]; |
||
828 | 26 | $stop = array_map([Compiler::class, 'pregQuote'], $stop); |
|
829 | 25 | // $stop[] = self::$commentMulti; |
|
830 | |||
831 | if ($rejectStrs !== null) { |
||
832 | 26 | $stop = array_merge($stop, $rejectStrs); |
|
833 | } |
||
834 | 26 | ||
835 | $patt = '(.*?)(' . implode('|', $stop) . ')'; |
||
836 | 26 | ||
837 | 26 | $nestingLevel = 0; |
|
838 | 26 | ||
839 | 25 | $content = []; |
|
840 | 25 | while ($this->match($patt, $m, false)) { |
|
841 | if (!empty($m[1])) { |
||
842 | $content[] = $m[1]; |
||
843 | if ($nestingOpen) { |
||
844 | $nestingLevel += substr_count($m[1], $nestingOpen); |
||
845 | 26 | } |
|
846 | } |
||
847 | 26 | ||
848 | 26 | $tok = $m[2]; |
|
849 | 14 | ||
850 | 14 | $this->count -= strlen($tok); |
|
851 | if ($tok == $end) { |
||
852 | if ($nestingLevel === 0) { |
||
853 | break; |
||
854 | } else { |
||
855 | $nestingLevel--; |
||
856 | 24 | } |
|
857 | 13 | } |
|
858 | 13 | ||
859 | if (($tok === "'" || $tok === '"') && $this->stringValue($str)) { |
||
860 | $content[] = $str; |
||
861 | 23 | continue; |
|
862 | 3 | } |
|
863 | 3 | ||
864 | if ($tok === '@{' && $this->interpolation($inter)) { |
||
865 | $content[] = $inter; |
||
866 | 23 | continue; |
|
867 | 23 | } |
|
868 | |||
869 | if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) { |
||
870 | break; |
||
871 | } |
||
872 | |||
873 | $content[] = $tok; |
||
874 | 26 | $this->count += strlen($tok); |
|
875 | } |
||
876 | 26 | ||
877 | 4 | $this->eatWhiteDefault = $oldWhite; |
|
878 | |||
879 | if (count($content) === 0) { |
||
880 | return false; |
||
881 | 25 | } |
|
882 | 25 | ||
883 | // trim the end |
||
884 | if (is_string(end($content))) { |
||
885 | 25 | $content[count($content) - 1] = rtrim(end($content)); |
|
886 | } |
||
887 | 25 | ||
888 | $out = ['string', '', $content]; |
||
889 | |||
890 | return true; |
||
891 | } |
||
892 | |||
893 | /** |
||
894 | * @param $out |
||
895 | 48 | * |
|
896 | * @return bool |
||
897 | 48 | */ |
|
898 | 48 | protected function stringValue(&$out) |
|
899 | 22 | { |
|
900 | 48 | $s = $this->seek(); |
|
901 | 12 | if ($this->literal('"', false)) { |
|
902 | $delim = '"'; |
||
903 | 48 | } elseif ($this->literal("'", false)) { |
|
904 | $delim = "'"; |
||
905 | } else { |
||
906 | 24 | return false; |
|
907 | } |
||
908 | |||
909 | $content = []; |
||
910 | 24 | ||
911 | // look for either ending delim , escape, or string interpolation |
||
912 | 24 | $patt = '([^\n]*?)(@\{|\\\\|' . |
|
913 | 24 | Compiler::pregQuote($delim) . ')'; |
|
914 | |||
915 | 24 | $oldWhite = $this->eatWhiteDefault; |
|
916 | 24 | $this->eatWhiteDefault = false; |
|
917 | 24 | ||
918 | 6 | while ($this->match($patt, $m, false)) { |
|
919 | 6 | $content[] = $m[1]; |
|
920 | 6 | if ($m[2] === '@{') { |
|
921 | $this->count -= strlen($m[2]); |
||
922 | 1 | if ($this->interpolation($inter)) { |
|
923 | 6 | $content[] = $inter; |
|
924 | } else { |
||
925 | 24 | $this->count += strlen($m[2]); |
|
926 | 2 | $content[] = '@{'; // ignore it |
|
927 | 2 | } |
|
928 | 2 | } elseif ($m[2] === '\\') { |
|
929 | $content[] = $m[2]; |
||
930 | if ($this->literal($delim, false)) { |
||
931 | 24 | $content[] = $delim; |
|
932 | 24 | } |
|
933 | } else { |
||
934 | $this->count -= strlen($delim); |
||
935 | break; // delim |
||
936 | 24 | } |
|
937 | } |
||
938 | 24 | ||
939 | 24 | $this->eatWhiteDefault = $oldWhite; |
|
940 | |||
941 | 24 | if ($this->literal($delim)) { |
|
942 | $out = ['string', $delim, $content]; |
||
943 | |||
944 | 1 | return true; |
|
945 | } |
||
946 | 1 | ||
947 | $this->seek($s); |
||
948 | |||
949 | return false; |
||
950 | } |
||
951 | |||
952 | /** |
||
953 | * @param $out |
||
954 | 19 | * |
|
955 | * @return bool |
||
956 | 19 | */ |
|
957 | 19 | protected function interpolation(&$out) |
|
958 | { |
||
959 | 19 | $oldWhite = $this->eatWhiteDefault; |
|
960 | 19 | $this->eatWhiteDefault = true; |
|
961 | 19 | ||
962 | 19 | $s = $this->seek(); |
|
963 | if ($this->literal('@{') && |
||
964 | 7 | $this->openString('}', $interp, null, ["'", '"', ';']) && |
|
965 | 7 | $this->literal('}', false) |
|
966 | 7 | ) { |
|
967 | 1 | $out = ['interpolate', $interp]; |
|
968 | $this->eatWhiteDefault = $oldWhite; |
||
969 | if ($this->eatWhiteDefault) { |
||
970 | 7 | $this->whitespace(); |
|
971 | } |
||
972 | |||
973 | 16 | return true; |
|
974 | 16 | } |
|
975 | |||
976 | 16 | $this->eatWhiteDefault = $oldWhite; |
|
977 | $this->seek($s); |
||
978 | |||
979 | return false; |
||
980 | } |
||
981 | |||
982 | /** |
||
983 | * @param $unit |
||
984 | 49 | * |
|
985 | * @return bool |
||
986 | */ |
||
987 | 49 | protected function unit(&$unit) |
|
988 | 49 | { |
|
989 | 49 | // speed shortcut |
|
990 | 49 | if (isset($this->buffer[$this->count])) { |
|
991 | $char = $this->buffer[$this->count]; |
||
992 | if (!ctype_digit($char) && $char !== '.') { |
||
993 | return false; |
||
994 | 49 | } |
|
995 | 38 | } |
|
996 | |||
997 | 38 | if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) { |
|
998 | $unit = ['number', $m[1], empty($m[2]) ? '' : $m[2]]; |
||
999 | |||
1000 | 49 | return true; |
|
1001 | } |
||
1002 | |||
1003 | return false; |
||
1004 | } |
||
1005 | |||
1006 | |||
1007 | /** |
||
1008 | * a # color |
||
1009 | * |
||
1010 | * @param $out |
||
1011 | 48 | * |
|
1012 | * @return bool |
||
1013 | 48 | */ |
|
1014 | 13 | protected function color(&$out) |
|
1015 | 1 | { |
|
1016 | if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) { |
||
1017 | 13 | if (strlen($m[1]) > 7) { |
|
1018 | $out = ['string', '', [$m[1]]]; |
||
1019 | } else { |
||
1020 | 13 | $out = ['raw_color', $m[1]]; |
|
1021 | } |
||
1022 | |||
1023 | 48 | return true; |
|
1024 | } |
||
1025 | |||
1026 | return false; |
||
1027 | } |
||
1028 | |||
1029 | /** |
||
1030 | * consume an argument definition list surrounded by () |
||
1031 | * each argument is a variable name with optional value |
||
1032 | * or at the end a ... or a variable named followed by ... |
||
1033 | * arguments are separated by , unless a ; is in the list, then ; is the |
||
1034 | * delimiter. |
||
1035 | * |
||
1036 | * @param $args |
||
1037 | * @param $isVararg |
||
1038 | * |
||
1039 | 45 | * @return bool |
|
1040 | * @throws \LesserPhp\Exception\GeneralException |
||
1041 | 45 | */ |
|
1042 | 45 | protected function argumentDef(&$args, &$isVararg) |
|
1043 | 45 | { |
|
1044 | $s = $this->seek(); |
||
1045 | if (!$this->literal('(')) { |
||
1046 | 19 | return false; |
|
1047 | 19 | } |
|
1048 | 19 | ||
1049 | $values = []; |
||
1050 | 19 | $delim = ','; |
|
1051 | 19 | $method = 'expressionList'; |
|
1052 | 19 | $value = []; |
|
1053 | 2 | $rhs = null; |
|
1054 | 2 | ||
1055 | $isVararg = false; |
||
1056 | while (true) { |
||
1057 | 19 | if ($this->literal('...')) { |
|
1058 | 16 | $isVararg = true; |
|
1059 | 16 | break; |
|
1060 | 16 | } |
|
1061 | |||
1062 | 16 | if ($this->$method($value)) { |
|
1063 | 9 | if ($value[0] === 'variable') { |
|
1064 | $arg = ['arg', $value[1]]; |
||
1065 | 13 | $ss = $this->seek(); |
|
1066 | 13 | ||
1067 | 2 | if ($this->assign() && $this->$method($rhs)) { |
|
1068 | 2 | $arg[] = $rhs; |
|
1069 | } else { |
||
1070 | $this->seek($ss); |
||
1071 | if ($this->literal('...')) { |
||
1072 | 16 | $arg[0] = 'rest'; |
|
1073 | 16 | $isVararg = true; |
|
1074 | 2 | } |
|
1075 | } |
||
1076 | 16 | ||
1077 | $values[] = $arg; |
||
1078 | 15 | if ($isVararg) { |
|
1079 | break; |
||
1080 | } |
||
1081 | continue; |
||
1082 | } else { |
||
1083 | 19 | $values[] = ['lit', $value]; |
|
1084 | 19 | } |
|
1085 | } |
||
1086 | 2 | ||
1087 | 2 | ||
1088 | 2 | if (!$this->literal($delim)) { |
|
1089 | if ($delim === ',' && $this->literal(';')) { |
||
1090 | // found new delim, convert existing args |
||
1091 | 2 | $delim = ';'; |
|
1092 | 2 | $method = 'propertyValue'; |
|
1093 | 2 | $newArg = null; |
|
1094 | 2 | ||
1095 | 2 | // transform arg list |
|
1096 | 2 | if (isset($values[1])) { // 2 items |
|
1097 | $newList = []; |
||
1098 | foreach ($values as $i => $arg) { |
||
1099 | 2 | switch ($arg[0]) { |
|
1100 | 2 | case 'arg': |
|
1101 | 2 | if ($i) { |
|
1102 | 2 | throw new GeneralException('Cannot mix ; and , as delimiter types'); |
|
1103 | 2 | } |
|
1104 | $newList[] = $arg[2]; |
||
1105 | 2 | break; |
|
1106 | case 'lit': |
||
1107 | $newList[] = $arg[1]; |
||
1108 | break; |
||
1109 | 2 | case 'rest': |
|
1110 | throw new GeneralException('Unexpected rest before semicolon'); |
||
1111 | 2 | } |
|
1112 | 2 | } |
|
1113 | 2 | ||
1114 | 2 | $newList = ['list', ', ', $newList]; |
|
1115 | 1 | ||
1116 | 1 | switch ($values[0][0]) { |
|
1117 | 2 | case 'arg': |
|
1118 | $newArg = ['arg', $values[0][1], $newList]; |
||
1119 | 2 | break; |
|
1120 | 2 | case 'lit': |
|
1121 | $newArg = ['lit', $newList]; |
||
1122 | break; |
||
1123 | 2 | } |
|
1124 | 2 | } elseif ($values) { // 1 item |
|
0 ignored issues
–
show
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 ![]() |
|||
1125 | $newArg = $values[0]; |
||
1126 | } |
||
1127 | 19 | ||
1128 | if ($newArg !== null) { |
||
1129 | $values = [$newArg]; |
||
1130 | } |
||
1131 | } else { |
||
1132 | 19 | break; |
|
1133 | } |
||
1134 | } |
||
1135 | } |
||
1136 | |||
1137 | if (!$this->literal(')')) { |
||
1138 | 19 | $this->seek($s); |
|
1139 | |||
1140 | 19 | return false; |
|
1141 | } |
||
1142 | |||
1143 | $args = $values; |
||
1144 | |||
1145 | return true; |
||
1146 | } |
||
1147 | |||
1148 | /** |
||
1149 | * consume a list of tags |
||
1150 | * this accepts a hanging delimiter |
||
1151 | * |
||
1152 | * @param array $tags |
||
1153 | 49 | * @param bool $simple |
|
1154 | * @param string $delim |
||
1155 | 49 | * |
|
1156 | 49 | * @return bool |
|
1157 | 46 | */ |
|
1158 | 46 | protected function tags(&$tags, $simple = false, $delim = ',') |
|
1159 | 46 | { |
|
1160 | $tags = []; |
||
1161 | while ($this->tag($tt, $simple)) { |
||
1162 | $tags[] = $tt; |
||
1163 | 49 | if (!$this->literal($delim)) { |
|
1164 | break; |
||
1165 | } |
||
1166 | } |
||
1167 | |||
1168 | return count($tags) !== 0; |
||
1169 | } |
||
1170 | |||
1171 | /** |
||
1172 | * list of tags of specifying mixin path |
||
1173 | * optionally separated by > (lazy, accepts extra >) |
||
1174 | 49 | * |
|
1175 | * @param array $tags |
||
1176 | 49 | * |
|
1177 | 49 | * @return bool |
|
1178 | 22 | */ |
|
1179 | 22 | protected function mixinTags(&$tags) |
|
1180 | { |
||
1181 | $tags = []; |
||
1182 | 49 | while ($this->tag($tt, true)) { |
|
1183 | $tags[] = $tt; |
||
1184 | $this->literal('>'); |
||
1185 | } |
||
1186 | |||
1187 | return count($tags) !== 0; |
||
1188 | } |
||
1189 | |||
1190 | /** |
||
1191 | * a bracketed value (contained within in a tag definition) |
||
1192 | * |
||
1193 | 49 | * @param array $parts |
|
1194 | * @param bool $hasExpression |
||
1195 | * |
||
1196 | 49 | * @return bool |
|
1197 | 47 | */ |
|
1198 | protected function tagBracket(&$parts, &$hasExpression) |
||
1199 | { |
||
1200 | 49 | // speed shortcut |
|
1201 | if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== '[') { |
||
1202 | 49 | return false; |
|
1203 | } |
||
1204 | 49 | ||
1205 | 3 | $s = $this->seek(); |
|
1206 | |||
1207 | 3 | $hasInterpolation = false; |
|
1208 | 3 | ||
1209 | 3 | if ($this->literal('[', false)) { |
|
1210 | 3 | $attrParts = ['[']; |
|
1211 | // keyword, string, operator |
||
1212 | while (true) { |
||
1213 | 3 | if ($this->literal(']', false)) { |
|
1214 | $this->count--; |
||
1215 | break; // get out early |
||
1216 | } |
||
1217 | 3 | ||
1218 | if ($this->match('\s+', $m)) { |
||
1219 | 3 | $attrParts[] = ' '; |
|
1220 | 3 | continue; |
|
1221 | } |
||
1222 | if ($this->stringValue($str)) { |
||
1223 | 3 | // escape parent selector, (yuck) |
|
1224 | 3 | foreach ($str[2] as &$chunk) { |
|
1225 | 3 | if (is_string($chunk)) { |
|
1226 | $chunk = str_replace($this->lessc->getParentSelector(), '$&$', $chunk); |
||
1227 | } |
||
1228 | 3 | } |
|
1229 | 3 | ||
1230 | 3 | $attrParts[] = $str; |
|
1231 | $hasInterpolation = true; |
||
1232 | continue; |
||
1233 | 3 | } |
|
1234 | 1 | ||
1235 | 1 | if ($this->keyword($word)) { |
|
1236 | 1 | $attrParts[] = $word; |
|
1237 | continue; |
||
1238 | } |
||
1239 | |||
1240 | 3 | if ($this->interpolation($inter)) { |
|
1241 | 3 | $attrParts[] = $inter; |
|
1242 | 3 | $hasInterpolation = true; |
|
1243 | continue; |
||
1244 | } |
||
1245 | |||
1246 | // operator, handles attr namespace too |
||
1247 | if ($this->match('[|-~\$\*\^=]+', $m)) { |
||
1248 | 3 | $attrParts[] = $m[0]; |
|
1249 | 3 | continue; |
|
1250 | 3 | } |
|
1251 | 3 | ||
1252 | break; |
||
1253 | 3 | } |
|
1254 | |||
1255 | 3 | if ($this->literal(']', false)) { |
|
1256 | $attrParts[] = ']'; |
||
1257 | foreach ($attrParts as $part) { |
||
1258 | $parts[] = $part; |
||
1259 | } |
||
1260 | 49 | $hasExpression = $hasExpression || $hasInterpolation; |
|
1261 | |||
1262 | 49 | return true; |
|
1263 | } |
||
1264 | $this->seek($s); |
||
1265 | } |
||
1266 | |||
1267 | $this->seek($s); |
||
1268 | |||
1269 | return false; |
||
1270 | } |
||
1271 | |||
1272 | /** |
||
1273 | 49 | * a space separated list of selectors |
|
1274 | * |
||
1275 | 49 | * @param $tag |
|
1276 | 49 | * @param bool $simple |
|
1277 | * |
||
1278 | 49 | * @return bool |
|
1279 | */ |
||
1280 | protected function tag(&$tag, $simple = false) |
||
1281 | 49 | { |
|
1282 | if ($simple) { |
||
1283 | 49 | $chars = '^@,:;{}\][>\(\) "\''; |
|
1284 | 49 | } else { |
|
1285 | 49 | $chars = '^@,;{}["\''; |
|
1286 | } |
||
1287 | |||
1288 | $s = $this->seek(); |
||
1289 | 49 | ||
1290 | 49 | $hasExpression = false; |
|
1291 | $parts = []; |
||
1292 | 49 | while ($this->tagBracket($parts, $hasExpression)) { |
|
1293 | 49 | ; |
|
1294 | 46 | } |
|
1295 | 46 | ||
1296 | 45 | $oldWhite = $this->eatWhiteDefault; |
|
1297 | $this->eatWhiteDefault = false; |
||
1298 | |||
1299 | 46 | while (true) { |
|
1300 | if ($this->match('([' . $chars . '0-9][' . $chars . ']*)', $m)) { |
||
1301 | $parts[] = $m[1]; |
||
1302 | 46 | if ($simple) { |
|
1303 | break; |
||
1304 | } |
||
1305 | 49 | ||
1306 | 13 | while ($this->tagBracket($parts, $hasExpression)) { |
|
1307 | 2 | ; |
|
1308 | 2 | } |
|
1309 | 2 | continue; |
|
1310 | 2 | } |
|
1311 | |||
1312 | if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') { |
||
1313 | 12 | if ($this->interpolation($interp)) { |
|
1314 | 12 | $hasExpression = true; |
|
1315 | 12 | $interp[2] = true; // don't unescape |
|
1316 | $parts[] = $interp; |
||
1317 | continue; |
||
1318 | } |
||
1319 | 49 | ||
1320 | 9 | if ($this->literal('@')) { |
|
1321 | 9 | $parts[] = '@'; |
|
1322 | 9 | continue; |
|
1323 | } |
||
1324 | } |
||
1325 | 49 | ||
1326 | if ($this->unit($unit)) { // for keyframes |
||
1327 | $parts[] = $unit[1]; |
||
1328 | 49 | $parts[] = $unit[2]; |
|
1329 | 49 | continue; |
|
1330 | 49 | } |
|
1331 | |||
1332 | 49 | break; |
|
1333 | } |
||
1334 | |||
1335 | 46 | $this->eatWhiteDefault = $oldWhite; |
|
1336 | 4 | if (!$parts) { |
|
0 ignored issues
–
show
The expression
$parts of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.
This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent. Consider making the comparison explicit by using ![]() |
|||
1337 | $this->seek($s); |
||
1338 | 46 | ||
1339 | return false; |
||
1340 | } |
||
1341 | 46 | ||
1342 | if ($hasExpression) { |
||
1343 | 46 | $tag = ['exp', ['string', '', $parts]]; |
|
1344 | } else { |
||
1345 | $tag = trim(implode($parts)); |
||
1346 | } |
||
1347 | |||
1348 | $this->whitespace(); |
||
1349 | |||
1350 | return true; |
||
1351 | } |
||
1352 | |||
1353 | 48 | /** |
|
1354 | * a css function |
||
1355 | 48 | * |
|
1356 | * @param array $func |
||
1357 | 48 | * |
|
1358 | 25 | * @return bool |
|
1359 | */ |
||
1360 | 25 | protected function func(&$func) |
|
1361 | { |
||
1362 | 25 | $s = $this->seek(); |
|
1363 | 25 | ||
1364 | 25 | if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) { |
|
1365 | $fname = $m[1]; |
||
1366 | 25 | ||
1367 | 1 | $sPreArgs = $this->seek(); |
|
1368 | |||
1369 | 24 | $args = []; |
|
1370 | 24 | while (true) { |
|
1371 | 21 | $ss = $this->seek(); |
|
1372 | // this ugly nonsense is for ie filter properties |
||
1373 | if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) { |
||
1374 | $args[] = ['string', '', [$name, '=', $value]]; |
||
1375 | 25 | } else { |
|
1376 | 25 | $this->seek($ss); |
|
1377 | if ($this->expressionList($value)) { |
||
1378 | $args[] = $value; |
||
1379 | 25 | } |
|
1380 | } |
||
1381 | 25 | ||
1382 | 25 | if (!$this->literal(',')) { |
|
1383 | break; |
||
1384 | 25 | } |
|
1385 | 7 | } |
|
1386 | $args = ['list', ',', $args]; |
||
1387 | 6 | ||
1388 | 6 | if ($this->literal(')')) { |
|
1389 | 6 | $func = ['function', $fname, $args]; |
|
1390 | |||
1391 | 6 | return true; |
|
1392 | } elseif ($fname === 'url') { |
||
1393 | // couldn't parse and in url? treat as string |
||
1394 | $this->seek($sPreArgs); |
||
1395 | if ($this->openString(')', $string) && $this->literal(')')) { |
||
1396 | 48 | $func = ['function', $fname, $string]; |
|
1397 | |||
1398 | 48 | return true; |
|
1399 | } |
||
1400 | } |
||
1401 | } |
||
1402 | |||
1403 | $this->seek($s); |
||
1404 | |||
1405 | return false; |
||
1406 | } |
||
1407 | |||
1408 | 49 | /** |
|
1409 | * consume a less variable |
||
1410 | 49 | * |
|
1411 | 49 | * @param $name |
|
1412 | 49 | * |
|
1413 | * @return bool |
||
1414 | 32 | */ |
|
1415 | 1 | protected function variable(&$name) |
|
1416 | { |
||
1417 | 32 | $s = $this->seek(); |
|
1418 | if ($this->literal($this->lessc->getVPrefix(), false) && |
||
1419 | ($this->variable($sub) || $this->keyword($name)) |
||
1420 | 32 | ) { |
|
1421 | if (!empty($sub)) { |
||
1422 | $name = ['variable', $sub]; |
||
1423 | 49 | } else { |
|
1424 | 49 | $name = $this->lessc->getVPrefix() . $name; |
|
1425 | } |
||
1426 | 49 | ||
1427 | return true; |
||
1428 | } |
||
1429 | |||
1430 | $name = null; |
||
1431 | $this->seek($s); |
||
1432 | |||
1433 | return false; |
||
1434 | } |
||
1435 | |||
1436 | /** |
||
1437 | 48 | * Consume an assignment operator |
|
1438 | * Can optionally take a name that will be set to the current property name |
||
1439 | 48 | * |
|
1440 | * @param string $name |
||
1441 | * |
||
1442 | * @return bool |
||
1443 | 48 | */ |
|
1444 | protected function assign($name = null) |
||
1445 | { |
||
1446 | if ($name !== null) { |
||
1447 | $this->currentProperty = $name; |
||
1448 | } |
||
1449 | |||
1450 | return $this->literal(':') || $this->literal('='); |
||
1451 | } |
||
1452 | |||
1453 | 49 | /** |
|
1454 | * consume a keyword |
||
1455 | 49 | * |
|
1456 | 48 | * @param $word |
|
1457 | * |
||
1458 | 48 | * @return bool |
|
1459 | */ |
||
1460 | protected function keyword(&$word) |
||
1461 | 49 | { |
|
1462 | if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) { |
||
1463 | $word = $m[1]; |
||
1464 | |||
1465 | return true; |
||
1466 | } |
||
1467 | |||
1468 | return false; |
||
1469 | 48 | } |
|
1470 | |||
1471 | 48 | /** |
|
1472 | 48 | * consume an end of statement delimiter |
|
1473 | 12 | * |
|
1474 | * @return bool |
||
1475 | 10 | */ |
|
1476 | protected function end() |
||
1477 | { |
||
1478 | 2 | if ($this->literal(';', false)) { |
|
1479 | return true; |
||
1480 | } elseif ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') { |
||
1481 | // if there is end of file or a closing block next then we don't need a ; |
||
1482 | return true; |
||
1483 | } |
||
1484 | |||
1485 | return false; |
||
1486 | 19 | } |
|
1487 | |||
1488 | 19 | /** |
|
1489 | * @param $guards |
||
1490 | 19 | * |
|
1491 | 19 | * @return bool |
|
1492 | */ |
||
1493 | 19 | protected function guards(&$guards) |
|
1494 | { |
||
1495 | $s = $this->seek(); |
||
1496 | 5 | ||
1497 | if (!$this->literal('when')) { |
||
1498 | 5 | $this->seek($s); |
|
1499 | 5 | ||
1500 | 5 | return false; |
|
1501 | 5 | } |
|
1502 | |||
1503 | $guards = []; |
||
1504 | |||
1505 | 5 | while ($this->guardGroup($g)) { |
|
1506 | $guards[] = $g; |
||
1507 | if (!$this->literal(',')) { |
||
1508 | break; |
||
1509 | } |
||
1510 | } |
||
1511 | |||
1512 | 5 | if (count($guards) === 0) { |
|
1513 | $guards = null; |
||
1514 | $this->seek($s); |
||
1515 | |||
1516 | return false; |
||
1517 | } |
||
1518 | |||
1519 | return true; |
||
1520 | } |
||
1521 | |||
1522 | 5 | /** |
|
1523 | * a bunch of guards that are and'd together |
||
1524 | 5 | * |
|
1525 | 5 | * @param $guardGroup |
|
1526 | 5 | * |
|
1527 | 5 | * @return bool |
|
1528 | 5 | */ |
|
1529 | 5 | protected function guardGroup(&$guardGroup) |
|
1530 | { |
||
1531 | $s = $this->seek(); |
||
1532 | $guardGroup = []; |
||
1533 | 5 | while ($this->guard($guard)) { |
|
1534 | $guardGroup[] = $guard; |
||
1535 | if (!$this->literal('and')) { |
||
1536 | break; |
||
1537 | } |
||
1538 | } |
||
1539 | |||
1540 | 5 | if (count($guardGroup) === 0) { |
|
1541 | $guardGroup = null; |
||
1542 | $this->seek($s); |
||
1543 | |||
1544 | return false; |
||
1545 | } |
||
1546 | |||
1547 | return true; |
||
1548 | 5 | } |
|
1549 | |||
1550 | 5 | /** |
|
1551 | 5 | * @param $guard |
|
1552 | * |
||
1553 | 5 | * @return bool |
|
1554 | 5 | */ |
|
1555 | 5 | protected function guard(&$guard) |
|
1556 | 1 | { |
|
1557 | $s = $this->seek(); |
||
1558 | $negate = $this->literal('not'); |
||
1559 | 5 | ||
1560 | if ($this->literal('(') && $this->expression($exp) && $this->literal(')')) { |
||
1561 | $guard = $exp; |
||
1562 | if ($negate) { |
||
1563 | $guard = ['negate', $guard]; |
||
1564 | } |
||
1565 | |||
1566 | return true; |
||
1567 | } |
||
1568 | |||
1569 | $this->seek($s); |
||
1570 | |||
1571 | return false; |
||
1572 | } |
||
1573 | |||
1574 | /* raw parsing functions */ |
||
1575 | 49 | ||
1576 | /** |
||
1577 | 49 | * @param string $what |
|
1578 | 49 | * @param bool $eatWhitespace |
|
1579 | * |
||
1580 | * @return bool |
||
1581 | */ |
||
1582 | 49 | protected function literal($what, $eatWhitespace = null) |
|
1583 | 49 | { |
|
1584 | 49 | if ($eatWhitespace === null) { |
|
1585 | 49 | $eatWhitespace = $this->eatWhiteDefault; |
|
1586 | } |
||
1587 | 49 | ||
1588 | // shortcut on single letter |
||
1589 | if (!isset($what[1]) && isset($this->buffer[$this->count])) { |
||
1590 | if ($this->buffer[$this->count] === $what) { |
||
1591 | 49 | if (!$eatWhitespace) { |
|
1592 | $this->count++; |
||
1593 | |||
1594 | return true; |
||
1595 | 49 | } |
|
1596 | 11 | // goes below... |
|
1597 | } else { |
||
1598 | return false; |
||
1599 | 49 | } |
|
1600 | } |
||
1601 | |||
1602 | if (!isset(self::$literalCache[$what])) { |
||
1603 | self::$literalCache[$what] = Compiler::pregQuote($what); |
||
1604 | } |
||
1605 | |||
1606 | return $this->match(self::$literalCache[$what], $m, $eatWhitespace); |
||
1607 | } |
||
1608 | |||
1609 | /** |
||
1610 | 3 | * @param $out |
|
1611 | * @param string $parseItem |
||
1612 | * @param string $delim |
||
1613 | 3 | * @param bool $flatten |
|
1614 | 3 | * |
|
1615 | 3 | * @return bool |
|
1616 | 3 | */ |
|
1617 | 3 | protected function genericList(&$out, $parseItem, $delim = "", $flatten = true) |
|
1618 | 3 | { |
|
1619 | 3 | // $parseItem is one of mediaQuery, mediaExpression |
|
1620 | $s = $this->seek(); |
||
1621 | $items = []; |
||
1622 | $value = null; |
||
1623 | while ($this->$parseItem($value)) { |
||
1624 | 3 | $items[] = $value; |
|
1625 | if ($delim) { |
||
1626 | if (!$this->literal($delim)) { |
||
1627 | break; |
||
1628 | } |
||
1629 | } |
||
1630 | 3 | } |
|
1631 | |||
1632 | if (count($items) === 0) { |
||
1633 | 3 | $this->seek($s); |
|
1634 | |||
1635 | return false; |
||
1636 | 3 | } |
|
1637 | |||
1638 | if ($flatten && count($items) === 1) { |
||
1639 | $out = $items[0]; |
||
1640 | } else { |
||
1641 | $out = ['list', $delim, $items]; |
||
1642 | } |
||
1643 | |||
1644 | return true; |
||
1645 | } |
||
1646 | |||
1647 | /** |
||
1648 | * try to match something on head of buffer |
||
1649 | * |
||
1650 | * @param string $regex |
||
1651 | * @param $out |
||
1652 | * @param bool $eatWhitespace |
||
1653 | * |
||
1654 | * @return bool |
||
1655 | */ |
||
1656 | protected function match($regex, &$out, $eatWhitespace = null) |
||
1657 | { |
||
1658 | if ($eatWhitespace === null) { |
||
1659 | $eatWhitespace = $this->eatWhiteDefault; |
||
1660 | } |
||
1661 | |||
1662 | $r = '/' . $regex . ($eatWhitespace && !$this->writeComments ? '\s*' : '') . '/Ais'; |
||
1663 | if (preg_match($r, $this->buffer, $out, null, $this->count)) { |
||
1664 | $this->count += strlen($out[0]); |
||
1665 | if ($eatWhitespace && $this->writeComments) { |
||
1666 | $this->whitespace(); |
||
1667 | } |
||
1668 | |||
1669 | return true; |
||
1670 | } |
||
1671 | |||
1672 | return false; |
||
1673 | } |
||
1674 | |||
1675 | /** |
||
1676 | * match some whitespace |
||
1677 | * |
||
1678 | * @return bool |
||
1679 | 49 | */ |
|
1680 | protected function whitespace() |
||
1681 | 49 | { |
|
1682 | 49 | if ($this->writeComments) { |
|
1683 | $gotWhite = false; |
||
1684 | while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) { |
||
1685 | 49 | if (isset($m[1]) && empty($this->seenComments[$this->count])) { |
|
1686 | 49 | $this->append(['comment', $m[1]]); |
|
1687 | 49 | $this->seenComments[$this->count] = true; |
|
1688 | 49 | } |
|
1689 | 1 | $this->count += mb_strlen($m[0]); |
|
1690 | $gotWhite = true; |
||
1691 | } |
||
1692 | 49 | ||
1693 | return $gotWhite; |
||
1694 | } |
||
1695 | 49 | ||
1696 | $this->match('', $m); |
||
1697 | |||
1698 | return mb_strlen($m[0]) > 0; |
||
1699 | } |
||
1700 | |||
1701 | /** |
||
1702 | * match something without consuming it |
||
1703 | 49 | * |
|
1704 | * @param string $regex |
||
1705 | 49 | * @param array $out |
|
1706 | 1 | * @param int $from |
|
1707 | 1 | * |
|
1708 | 1 | * @return int |
|
1709 | 1 | */ |
|
1710 | 1 | protected function peek($regex, &$out = null, $from = null) |
|
1711 | { |
||
1712 | 1 | if ($from === null) { |
|
1713 | 1 | $from = $this->count; |
|
1714 | } |
||
1715 | $r = '/' . $regex . '/Ais'; |
||
1716 | 1 | ||
1717 | return preg_match($r, $this->buffer, $out, null, $from); |
||
1718 | } |
||
1719 | 49 | ||
1720 | 49 | /** |
|
1721 | * seek to a spot in the buffer or return where we are on no argument |
||
1722 | * |
||
1723 | * @param int $where |
||
1724 | * |
||
1725 | * @return int |
||
1726 | */ |
||
1727 | protected function seek($where = null) |
||
1728 | { |
||
1729 | if ($where !== null) { |
||
1730 | $this->count = $where; |
||
1731 | } |
||
1732 | 24 | ||
1733 | return $this->count; |
||
1734 | 24 | } |
|
1735 | 20 | ||
1736 | /* misc functions */ |
||
1737 | 24 | ||
1738 | /** |
||
1739 | 24 | * @param string $msg |
|
1740 | * @param int $count |
||
1741 | * |
||
1742 | * @throws \LesserPhp\Exception\GeneralException |
||
1743 | */ |
||
1744 | public function throwError($msg = 'parse error', $count = null) |
||
1745 | { |
||
1746 | $count = $count === null ? $this->count : $count; |
||
1747 | |||
1748 | $line = $this->line + substr_count(substr($this->buffer, 0, $count), "\n"); |
||
1749 | 49 | ||
1750 | if (!empty($this->sourceName)) { |
||
1751 | 49 | $loc = "$this->sourceName on line $line"; |
|
1752 | 49 | } else { |
|
1753 | $loc = "line: $line"; |
||
1754 | } |
||
1755 | 49 | ||
1756 | // TODO this depends on $this->count |
||
1757 | if ($this->peek("(.*?)(\n|$)", $m, $count)) { |
||
1758 | throw new GeneralException("$msg: failed at `$m[1]` $loc"); |
||
1759 | } else { |
||
1760 | throw new GeneralException("$msg: $loc"); |
||
1761 | } |
||
1762 | } |
||
1763 | |||
1764 | /** |
||
1765 | * @param array|null $selectors |
||
1766 | 5 | * @param string|null $type |
|
1767 | * |
||
1768 | 5 | * @return Block |
|
1769 | */ |
||
1770 | 5 | protected function pushBlock(array $selectors = null, $type = null) |
|
1771 | { |
||
1772 | 5 | $this->env = Block::factory($this, self::$nextBlockId++, $this->count, $type, $selectors, $this->env); |
|
1773 | |||
1774 | return $this->env; |
||
1775 | 5 | } |
|
1776 | |||
1777 | /** |
||
1778 | * push a block that doesn't multiply tags |
||
1779 | 5 | * |
|
1780 | 5 | * @param string $type |
|
1781 | * |
||
1782 | * @return Block|Block\Directive|Block\Media |
||
1783 | */ |
||
1784 | protected function pushSpecialBlock($type) |
||
1785 | { |
||
1786 | return $this->pushBlock(null, $type); |
||
1787 | } |
||
1788 | |||
1789 | /** |
||
1790 | * append a property to the current block |
||
1791 | * |
||
1792 | 49 | * @param $prop |
|
1793 | * @param int $pos |
||
1794 | 49 | */ |
|
1795 | 49 | protected function append($prop, $pos = null) |
|
1796 | { |
||
1797 | 49 | if ($pos !== null) { |
|
1798 | 49 | $prop[-1] = $pos; |
|
1799 | } |
||
1800 | 49 | ||
1801 | 49 | $property = Property::factoryFromOldFormat($prop, $pos); |
|
1802 | |||
1803 | 49 | $this->env->props[] = $property; |
|
1804 | 49 | } |
|
1805 | |||
1806 | /** |
||
1807 | * pop something off the stack |
||
1808 | * |
||
1809 | 49 | * @return Block |
|
1810 | */ |
||
1811 | protected function pop() |
||
1812 | 49 | { |
|
1813 | $old = $this->env; |
||
1814 | 49 | $this->env = $this->env->parent; |
|
1815 | |||
1816 | 49 | return $old; |
|
1817 | } |
||
1818 | |||
1819 | /** |
||
1820 | * remove comments from $text |
||
1821 | * todo: make it work for all functions, not just url |
||
1822 | * |
||
1823 | * @param string $text |
||
1824 | * |
||
1825 | * @return string |
||
1826 | 49 | */ |
|
1827 | protected function removeComments($text) |
||
1828 | 49 | { |
|
1829 | $look = [ |
||
1830 | 'url(', |
||
1831 | '//', |
||
1832 | '/*', |
||
1833 | '"', |
||
1834 | "'", |
||
1835 | ]; |
||
1836 | |||
1837 | 49 | $out = ''; |
|
1838 | $min = null; |
||
1839 | 49 | while (true) { |
|
1840 | 49 | // find the next item |
|
1841 | foreach ($look as $token) { |
||
1842 | 49 | $pos = mb_strpos($text, $token); |
|
1843 | 49 | if ($pos !== false) { |
|
1844 | if ($min === null || $pos < $min[1]) { |
||
1845 | $min = [$token, $pos]; |
||
1846 | } |
||
1847 | } |
||
1848 | } |
||
1849 | |||
1850 | 46 | if ($min === null) { |
|
1851 | break; |
||
1852 | 46 | } |
|
1853 | 46 | ||
1854 | $count = $min[1]; |
||
1855 | 46 | $skip = 0; |
|
1856 | $newlines = 0; |
||
1857 | switch ($min[0]) { |
||
1858 | case 'url(': |
||
1859 | if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) { |
||
1860 | $count += mb_strlen($m[0]) - mb_strlen($min[0]); |
||
1861 | } |
||
1862 | break; |
||
1863 | case '"': |
||
1864 | case "'": |
||
1865 | if (preg_match('/' . $min[0] . '.*?(?<!\\\\)' . $min[0] . '/', $text, $m, 0, $count)) { |
||
1866 | 49 | $count += mb_strlen($m[0]) - 1; |
|
1867 | } |
||
1868 | break; |
||
1869 | 49 | case '//': |
|
1870 | $skip = mb_strpos($text, "\n", $count); |
||
1871 | if ($skip === false) { |
||
1872 | $skip = mb_strlen($text) - $count; |
||
1873 | } else { |
||
1874 | $skip -= $count; |
||
1875 | } |
||
1876 | 49 | break; |
|
1877 | 49 | case '/*': |
|
1878 | 49 | if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) { |
|
1879 | $skip = mb_strlen($m[0]); |
||
1880 | 49 | $newlines = mb_substr_count($m[0], "\n"); |
|
1881 | 49 | } |
|
1882 | 49 | break; |
|
1883 | 27 | } |
|
1884 | 49 | ||
1885 | if ($skip === 0) { |
||
1886 | $count += mb_strlen($min[0]); |
||
1887 | } |
||
1888 | |||
1889 | 49 | $out .= mb_substr($text, 0, $count) . str_repeat("\n", $newlines); |
|
1890 | 49 | $text = mb_substr($text, $count + $skip); |
|
1891 | |||
1892 | $min = null; |
||
1893 | 27 | } |
|
1894 | 27 | ||
1895 | 27 | return $out . $text; |
|
1896 | 27 | } |
|
1897 | 27 | ||
1898 | 6 | /** |
|
1899 | 6 | * @param bool $writeComments |
|
1900 | */ |
||
1901 | 6 | public function setWriteComments($writeComments) |
|
1902 | 27 | { |
|
1903 | 22 | $this->writeComments = $writeComments; |
|
1904 | 24 | } |
|
1905 | 24 | ||
1906 | /** |
||
1907 | 24 | * @param int $s |
|
1908 | 18 | * |
|
1909 | 18 | * @return bool |
|
1910 | 18 | */ |
|
1911 | protected function handleLiteralMedia($s) |
||
1912 | { |
||
1913 | 18 | // seriously, this || true is required for this statement to work!? |
|
1914 | if (($this->mediaQueryList($mediaQueries) || true) && $this->literal('{')) { |
||
1915 | 18 | $media = $this->pushSpecialBlock('media'); |
|
1916 | 4 | $media->queries = $mediaQueries === null ? [] : $mediaQueries; |
|
0 ignored issues
–
show
The property
queries does not seem to exist in LesserPhp\Block .
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name. If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading. ![]() |
|||
1917 | 4 | ||
1918 | 4 | return true; |
|
1919 | 4 | } else { |
|
1920 | $this->seek($s); |
||
1921 | 4 | } |
|
1922 | |||
1923 | return false; |
||
1924 | 27 | } |
|
1925 | 24 | ||
1926 | /** |
||
1927 | * @param string $directiveName |
||
1928 | 27 | * |
|
1929 | 27 | * @return bool |
|
1930 | */ |
||
1931 | 27 | protected function handleDirectiveBlock($directiveName) |
|
1932 | { |
||
1933 | // seriously, this || true is required for this statement to work!? |
||
1934 | 49 | if (($this->openString('{', $directiveValue, null, [';']) || true) && $this->literal('{')) { |
|
1935 | $dir = $this->pushSpecialBlock('directive'); |
||
1936 | $dir->name = $directiveName; |
||
0 ignored issues
–
show
The property
name does not seem to exist in LesserPhp\Block .
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name. If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading. ![]() |
|||
1937 | if ($directiveValue !== null) { |
||
1938 | $dir->value = $directiveValue; |
||
0 ignored issues
–
show
The property
value does not seem to exist in LesserPhp\Block .
An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name. If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading. ![]() |
|||
1939 | } |
||
1940 | 49 | ||
1941 | return true; |
||
1942 | 49 | } |
|
1943 | 49 | ||
1944 | return false; |
||
1945 | } |
||
1946 | |||
1947 | /** |
||
1948 | * @param string $directiveName |
||
1949 | * |
||
1950 | 3 | * @return bool |
|
1951 | */ |
||
1952 | protected function handleDirectiveLine($directiveName) |
||
1953 | 3 | { |
|
1954 | 3 | if ($this->propertyValue($directiveValue) && $this->end()) { |
|
1955 | 3 | $this->append(['directive', $directiveName, $directiveValue]); |
|
1956 | |||
1957 | 3 | return true; |
|
1958 | } |
||
1959 | |||
1960 | return false; |
||
1961 | } |
||
1962 | |||
1963 | /** |
||
1964 | * @param string $directiveName |
||
1965 | * |
||
1966 | * @return bool |
||
1967 | */ |
||
1968 | protected function handleRulesetDefinition($directiveName) |
||
1969 | { |
||
1970 | 4 | //Ruleset Definition |
|
1971 | $this->openString('{', $directiveValue, null, [';']); |
||
1972 | |||
1973 | 4 | if ($this->literal('{')) { |
|
1974 | 4 | $dir = $this->pushBlock($this->fixTags(['@' . $directiveName]), 'ruleset'); |
|
1975 | 4 | if (!$dir instanceof Block\Ruleset) { |
|
1976 | 4 | throw new \RuntimeException('Block factory did not produce a Ruleset'); |
|
1977 | 2 | } |
|
1978 | |||
1979 | $dir->name = $directiveName; |
||
1980 | 4 | if ($directiveValue !== null) { |
|
1981 | $dir->value = $directiveValue; |
||
1982 | } |
||
1983 | 1 | ||
1984 | return true; |
||
1985 | } |
||
1986 | |||
1987 | return false; |
||
1988 | } |
||
1989 | |||
1990 | private function clearBlockStack() |
||
1991 | 1 | { |
|
1992 | $this->env = null; |
||
1993 | 1 | } |
|
1994 | } |
||
1995 |
In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:
Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion: