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