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 | /** |
||
6 | * lesserphp |
||
7 | * https://www.maswaba.de/lesserphp |
||
8 | * |
||
9 | * LESS CSS compiler, adapted from http://lesscss.org |
||
10 | * |
||
11 | * Copyright 2013, Leaf Corcoran <[email protected]> |
||
12 | * Copyright 2016, Marcus Schwarz <[email protected]> |
||
13 | * Licensed under MIT or GPLv3, see LICENSE |
||
14 | * @package LesserPhp |
||
15 | */ |
||
16 | use LesserPhp\Color\Converter; |
||
17 | use LesserPhp\Exception\GeneralException; |
||
18 | use LesserPhp\Library\Assertions; |
||
19 | use LesserPhp\Library\Coerce; |
||
20 | use LesserPhp\Library\Functions; |
||
21 | |||
22 | /** |
||
23 | * The LESS compiler and parser. |
||
24 | * |
||
25 | * Converting LESS to CSS is a three stage process. The incoming file is parsed |
||
26 | * by `lessc_parser` into a syntax tree, then it is compiled into another tree |
||
27 | * representing the CSS structure by `lessc`. The CSS tree is fed into a |
||
28 | * formatter, like `lessc_formatter` which then outputs CSS as a string. |
||
29 | * |
||
30 | * During the first compile, all values are *reduced*, which means that their |
||
31 | * types are brought to the lowest form before being dump as strings. This |
||
32 | * handles math equations, variable dereferences, and the like. |
||
33 | * |
||
34 | * The `parse` function of `lessc` is the entry point. |
||
35 | * |
||
36 | * In summary: |
||
37 | * |
||
38 | * The `lessc` class creates an instance of the parser, feeds it LESS code, |
||
39 | * then transforms the resulting tree to a CSS tree. This class also holds the |
||
40 | * evaluation context, such as all available mixins and variables at any given |
||
41 | * time. |
||
42 | * |
||
43 | * The `lessc_parser` class is only concerned with parsing its input. |
||
44 | * |
||
45 | * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string, |
||
46 | * handling things like indentation. |
||
47 | */ |
||
48 | class Compiler |
||
49 | { |
||
50 | |||
51 | const VERSION = 'v0.5.1'; |
||
52 | |||
53 | static public $TRUE = ['keyword', 'true']; |
||
54 | static public $FALSE = ['keyword', 'false']; |
||
55 | |||
56 | /** |
||
57 | * @var callable[] |
||
58 | */ |
||
59 | private $libFunctions = []; |
||
60 | |||
61 | /** |
||
62 | * @var string[] |
||
63 | */ |
||
64 | private $registeredVars = []; |
||
65 | |||
66 | /** |
||
67 | * @var bool |
||
68 | */ |
||
69 | protected $preserveComments = false; |
||
70 | |||
71 | /** |
||
72 | * @var string $vPrefix prefix of abstract properties |
||
73 | */ |
||
74 | private $vPrefix = '@'; |
||
75 | |||
76 | /** |
||
77 | * @var string $mPrefix prefix of abstract blocks |
||
78 | */ |
||
79 | private $mPrefix = '$'; |
||
80 | |||
81 | /** |
||
82 | * @var string |
||
83 | */ |
||
84 | private $parentSelector = '&'; |
||
85 | |||
86 | /** |
||
87 | * @var bool $importDisabled disable @import |
||
88 | */ |
||
89 | private $importDisabled = false; |
||
90 | |||
91 | /** |
||
92 | * @var string[] |
||
93 | */ |
||
94 | private $importDirs = []; |
||
95 | |||
96 | /** |
||
97 | * @var int |
||
98 | */ |
||
99 | private $numberPrecision; |
||
100 | |||
101 | /** |
||
102 | * @var string[] |
||
103 | */ |
||
104 | private $allParsedFiles = []; |
||
105 | |||
106 | /** |
||
107 | * set to the parser that generated the current line when compiling |
||
108 | * so we know how to create error messages |
||
109 | * @var \LesserPhp\Parser |
||
110 | */ |
||
111 | private $sourceParser; |
||
112 | |||
113 | /** |
||
114 | * @var integer $sourceLoc Lines of Code |
||
115 | */ |
||
116 | private $sourceLoc; |
||
117 | |||
118 | /** |
||
119 | * @var int $nextImportId uniquely identify imports |
||
120 | */ |
||
121 | static private $nextImportId = 0; |
||
122 | |||
123 | /** |
||
124 | * @var Parser |
||
125 | */ |
||
126 | private $parser; |
||
127 | |||
128 | /** |
||
129 | * @var \LesserPhp\Formatter\FormatterInterface |
||
130 | */ |
||
131 | private $formatter; |
||
132 | |||
133 | /** |
||
134 | * @var \LesserPhp\NodeEnv What's the meaning of "env" in this context? |
||
135 | */ |
||
136 | private $env; |
||
137 | |||
138 | /** |
||
139 | * @var \LesserPhp\Library\Coerce |
||
140 | */ |
||
141 | private $coerce; |
||
142 | |||
143 | /** |
||
144 | * @var \LesserPhp\Library\Assertions |
||
145 | */ |
||
146 | private $assertions; |
||
147 | |||
148 | /** |
||
149 | * @var \LesserPhp\Library\Functions |
||
150 | */ |
||
151 | private $functions; |
||
152 | |||
153 | /** |
||
154 | * @var mixed what's this exactly? |
||
155 | */ |
||
156 | private $scope; |
||
157 | |||
158 | /** |
||
159 | * @var string |
||
160 | */ |
||
161 | private $formatterName; |
||
162 | |||
163 | /** |
||
164 | * @var \LesserPhp\Color\Converter |
||
165 | */ |
||
166 | private $converter; |
||
167 | |||
168 | /** |
||
169 | * Constructor. |
||
170 | * |
||
171 | * Hardwires dependencies for now |
||
172 | */ |
||
173 | 49 | public function __construct() |
|
174 | { |
||
175 | 49 | $this->coerce = new Coerce(); |
|
176 | 49 | $this->assertions = new Assertions($this->coerce); |
|
177 | 49 | $this->converter = new Converter(); |
|
178 | 49 | $this->functions = new Functions($this->assertions, $this->coerce, $this, $this->converter); |
|
179 | 49 | } |
|
180 | |||
181 | /** |
||
182 | * attempts to find the path of an import url, returns null for css files |
||
183 | * |
||
184 | * @param $url |
||
185 | * |
||
186 | * @return null|string |
||
187 | */ |
||
188 | 3 | View Code Duplication | protected function findImport($url) |
189 | { |
||
190 | 3 | foreach ($this->importDirs as $dir) { |
|
191 | 3 | $full = $dir . (mb_substr($dir, -1) !== '/' ? '/' : '') . $url; |
|
192 | 3 | if ($this->fileExists($file = $full . '.less') || $this->fileExists($file = $full)) { |
|
193 | 3 | return $file; |
|
194 | } |
||
195 | } |
||
196 | |||
197 | 2 | return null; |
|
198 | } |
||
199 | |||
200 | /** |
||
201 | * @param string $name |
||
202 | * |
||
203 | * @return bool |
||
204 | */ |
||
205 | 3 | protected function fileExists($name) |
|
206 | { |
||
207 | 3 | return is_file($name); |
|
208 | } |
||
209 | |||
210 | /** |
||
211 | * @param array $items |
||
212 | * @param $delim |
||
213 | * |
||
214 | * @return array |
||
215 | */ |
||
216 | 48 | public static function compressList(array $items, $delim) |
|
217 | { |
||
218 | 48 | if (!isset($items[1]) && isset($items[0])) { |
|
219 | 48 | return $items[0]; |
|
220 | } else { |
||
221 | 27 | return ['list', $delim, $items]; |
|
222 | } |
||
223 | } |
||
224 | |||
225 | /** |
||
226 | * @param string $what |
||
227 | * |
||
228 | * @return string |
||
229 | */ |
||
230 | 36 | public static function pregQuote($what) |
|
231 | { |
||
232 | 36 | return preg_quote($what, '/'); |
|
233 | } |
||
234 | |||
235 | /** |
||
236 | * @param array $importPath |
||
237 | * @param $parentBlock |
||
238 | * @param $out |
||
239 | * |
||
240 | * @return array|false |
||
241 | * @throws \LesserPhp\Exception\GeneralException |
||
242 | */ |
||
243 | 3 | protected function tryImport(array $importPath, $parentBlock, $out) |
|
244 | { |
||
245 | 3 | if ($importPath[0] === 'function' && $importPath[1] === 'url') { |
|
246 | 2 | $importPath = $this->flattenList($importPath[2]); |
|
247 | } |
||
248 | |||
249 | 3 | $str = $this->coerce->coerceString($importPath); |
|
250 | 3 | if ($str === null) { |
|
251 | 2 | return false; |
|
252 | } |
||
253 | |||
254 | 3 | $url = $this->compileValue($this->functions->e($str)); |
|
255 | |||
256 | // don't import if it ends in css |
||
257 | 3 | if (substr_compare($url, '.css', -4, 4) === 0) { |
|
258 | return false; |
||
259 | } |
||
260 | |||
261 | 3 | $realPath = $this->findImport($url); |
|
262 | |||
263 | 3 | if ($realPath === null) { |
|
264 | 2 | return false; |
|
265 | } |
||
266 | |||
267 | 3 | if ($this->isImportDisabled()) { |
|
268 | 1 | return [false, '/* import disabled */']; |
|
269 | } |
||
270 | |||
271 | 2 | if (isset($this->allParsedFiles[realpath($realPath)])) { |
|
272 | 2 | return [false, null]; |
|
273 | } |
||
274 | |||
275 | 2 | $this->addParsedFile($realPath); |
|
276 | 2 | $parser = $this->makeParser($realPath); |
|
277 | 2 | $root = $parser->parse(file_get_contents($realPath)); |
|
278 | |||
279 | // set the parents of all the block props |
||
280 | 2 | foreach ($root->props as $prop) { |
|
281 | 2 | if ($prop[0] === 'block') { |
|
282 | 2 | $prop[1]->parent = $parentBlock; |
|
283 | } |
||
284 | } |
||
285 | |||
286 | // copy mixins into scope, set their parents |
||
287 | // bring blocks from import into current block |
||
288 | // TODO: need to mark the source parser these came from this file |
||
289 | 2 | foreach ($root->children as $childName => $child) { |
|
290 | 2 | if (isset($parentBlock->children[$childName])) { |
|
291 | 2 | $parentBlock->children[$childName] = array_merge( |
|
292 | 2 | $parentBlock->children[$childName], |
|
293 | $child |
||
294 | ); |
||
295 | } else { |
||
296 | 2 | $parentBlock->children[$childName] = $child; |
|
297 | } |
||
298 | } |
||
299 | |||
300 | 2 | $pi = pathinfo($realPath); |
|
301 | 2 | $dir = $pi["dirname"]; |
|
302 | |||
303 | 2 | list($top, $bottom) = $this->sortProps($root->props, true); |
|
304 | 2 | $this->compileImportedProps($top, $parentBlock, $out, $dir); |
|
305 | |||
306 | 2 | return [true, $bottom, $parser, $dir]; |
|
307 | } |
||
308 | |||
309 | /** |
||
310 | * @param array $props |
||
311 | * @param $block |
||
312 | * @param $out |
||
313 | * @param string $importDir |
||
314 | */ |
||
315 | 2 | protected function compileImportedProps(array $props, $block, $out, $importDir) |
|
316 | { |
||
317 | 2 | $oldSourceParser = $this->sourceParser; |
|
318 | |||
319 | 2 | $oldImport = $this->importDirs; |
|
320 | |||
321 | 2 | array_unshift($this->importDirs, $importDir); |
|
322 | |||
323 | 2 | foreach ($props as $prop) { |
|
324 | 2 | $this->compileProp($prop, $block, $out); |
|
325 | } |
||
326 | |||
327 | 2 | $this->importDirs = $oldImport; |
|
328 | 2 | $this->sourceParser = $oldSourceParser; |
|
329 | 2 | } |
|
330 | |||
331 | /** |
||
332 | * Recursively compiles a block. |
||
333 | * |
||
334 | * A block is analogous to a CSS block in most cases. A single LESS document |
||
335 | * is encapsulated in a block when parsed, but it does not have parent tags |
||
336 | * so all of it's children appear on the root level when compiled. |
||
337 | * |
||
338 | * Blocks are made up of props and children. |
||
339 | * |
||
340 | * Props are property instructions, array tuples which describe an action |
||
341 | * to be taken, eg. write a property, set a variable, mixin a block. |
||
342 | * |
||
343 | * The children of a block are just all the blocks that are defined within. |
||
344 | * This is used to look up mixins when performing a mixin. |
||
345 | * |
||
346 | * Compiling the block involves pushing a fresh environment on the stack, |
||
347 | * and iterating through the props, compiling each one. |
||
348 | * |
||
349 | * See lessc::compileProp() |
||
350 | * |
||
351 | * @param $block |
||
352 | */ |
||
353 | 49 | protected function compileBlock($block) |
|
354 | { |
||
355 | 49 | switch ($block->type) { |
|
356 | 49 | case "root": |
|
357 | 49 | $this->compileRoot($block); |
|
358 | 38 | break; |
|
359 | 46 | case null: |
|
360 | 46 | $this->compileCSSBlock($block); |
|
361 | 35 | break; |
|
362 | 6 | case "media": |
|
363 | 3 | $this->compileMedia($block); |
|
364 | 3 | break; |
|
365 | 4 | case "directive": |
|
366 | 4 | $name = "@" . $block->name; |
|
367 | 4 | if (!empty($block->value)) { |
|
368 | 2 | $name .= " " . $this->compileValue($this->reduce($block->value)); |
|
369 | } |
||
370 | |||
371 | 4 | $this->compileNestedBlock($block, [$name]); |
|
372 | 4 | break; |
|
373 | default: |
||
374 | $block->parser->throwError("unknown block type: $block->type\n", $block->count); |
||
375 | } |
||
376 | 38 | } |
|
377 | |||
378 | /** |
||
379 | * @param $block |
||
380 | */ |
||
381 | 46 | protected function compileCSSBlock($block) |
|
382 | { |
||
383 | 46 | $env = $this->pushEnv($this->env); |
|
384 | |||
385 | 46 | $selectors = $this->compileSelectors($block->tags); |
|
386 | 46 | $env->setSelectors($this->multiplySelectors($selectors)); |
|
387 | 46 | $out = $this->makeOutputBlock(null, $env->getSelectors()); |
|
388 | |||
389 | 46 | $this->scope->children[] = $out; |
|
390 | 46 | $this->compileProps($block, $out); |
|
391 | |||
392 | 35 | $block->scope = $env; // mixins carry scope with them! |
|
393 | 35 | $this->popEnv(); |
|
394 | 35 | } |
|
395 | |||
396 | /** |
||
397 | * @param $media |
||
398 | */ |
||
399 | 3 | protected function compileMedia($media) |
|
400 | { |
||
401 | 3 | $env = $this->pushEnv($this->env, $media); |
|
402 | 3 | $parentScope = $this->mediaParent($this->scope); |
|
403 | |||
404 | 3 | $query = $this->compileMediaQuery($this->multiplyMedia($env)); |
|
405 | |||
406 | 3 | $this->scope = $this->makeOutputBlock($media->type, [$query]); |
|
407 | 3 | $parentScope->children[] = $this->scope; |
|
408 | |||
409 | 3 | $this->compileProps($media, $this->scope); |
|
410 | |||
411 | 3 | if (count($this->scope->lines) > 0) { |
|
412 | 3 | $orphanSelelectors = $this->findClosestSelectors(); |
|
413 | 3 | if ($orphanSelelectors !== null) { |
|
414 | 3 | $orphan = $this->makeOutputBlock(null, $orphanSelelectors); |
|
415 | 3 | $orphan->lines = $this->scope->lines; |
|
416 | 3 | array_unshift($this->scope->children, $orphan); |
|
417 | 3 | $this->scope->lines = []; |
|
418 | } |
||
419 | } |
||
420 | |||
421 | 3 | $this->scope = $this->scope->parent; |
|
422 | 3 | $this->popEnv(); |
|
423 | 3 | } |
|
424 | |||
425 | /** |
||
426 | * @param $scope |
||
427 | * |
||
428 | * @return mixed |
||
429 | */ |
||
430 | 3 | protected function mediaParent($scope) |
|
431 | { |
||
432 | 3 | while (!empty($scope->parent)) { |
|
433 | 1 | if (!empty($scope->type) && $scope->type !== "media") { |
|
434 | 1 | break; |
|
435 | } |
||
436 | 1 | $scope = $scope->parent; |
|
437 | } |
||
438 | |||
439 | 3 | return $scope; |
|
440 | } |
||
441 | |||
442 | /** |
||
443 | * @param $block |
||
444 | * @param $selectors |
||
445 | */ |
||
446 | 4 | protected function compileNestedBlock($block, $selectors) |
|
447 | { |
||
448 | 4 | $this->pushEnv($this->env, $block); |
|
449 | 4 | $this->scope = $this->makeOutputBlock($block->type, $selectors); |
|
450 | 4 | $this->scope->parent->children[] = $this->scope; |
|
451 | |||
452 | 4 | $this->compileProps($block, $this->scope); |
|
453 | |||
454 | 4 | $this->scope = $this->scope->parent; |
|
455 | 4 | $this->popEnv(); |
|
456 | 4 | } |
|
457 | |||
458 | /** |
||
459 | * @param $root |
||
460 | */ |
||
461 | 49 | protected function compileRoot($root) |
|
462 | { |
||
463 | 49 | $this->pushEnv($this->env); |
|
464 | 49 | $this->scope = $this->makeOutputBlock($root->type); |
|
465 | 49 | $this->compileProps($root, $this->scope); |
|
466 | 38 | $this->popEnv(); |
|
467 | 38 | } |
|
468 | |||
469 | /** |
||
470 | * @param $block |
||
471 | * @param $out |
||
472 | */ |
||
473 | 49 | protected function compileProps($block, $out) |
|
474 | { |
||
475 | 49 | foreach ($this->sortProps($block->props) as $prop) { |
|
476 | 49 | $this->compileProp($prop, $block, $out); |
|
477 | } |
||
478 | 38 | $out->lines = $this->deduplicate($out->lines); |
|
479 | 38 | } |
|
480 | |||
481 | /** |
||
482 | * Deduplicate lines in a block. Comments are not deduplicated. If a |
||
483 | * duplicate rule is detected, the comments immediately preceding each |
||
484 | * occurence are consolidated. |
||
485 | * |
||
486 | * @param array $lines |
||
487 | * |
||
488 | * @return array |
||
489 | */ |
||
490 | 38 | protected function deduplicate(array $lines) |
|
491 | { |
||
492 | 38 | $unique = []; |
|
493 | 38 | $comments = []; |
|
494 | |||
495 | 38 | foreach ($lines as $line) { |
|
496 | 38 | if (strpos($line, '/*') === 0) { |
|
497 | 2 | $comments[] = $line; |
|
498 | 2 | continue; |
|
499 | } |
||
500 | 37 | if (!in_array($line, $unique)) { |
|
501 | 37 | $unique[] = $line; |
|
502 | } |
||
503 | 37 | array_splice($unique, array_search($line, $unique), 0, $comments); |
|
504 | 37 | $comments = []; |
|
505 | } |
||
506 | |||
507 | 38 | return array_merge($unique, $comments); |
|
508 | } |
||
509 | |||
510 | /** |
||
511 | * @param array $props |
||
512 | * @param bool $split |
||
513 | * |
||
514 | * @return array |
||
515 | */ |
||
516 | 49 | protected function sortProps(array $props, $split = false) |
|
517 | { |
||
518 | 49 | $vars = []; |
|
519 | 49 | $imports = []; |
|
520 | 49 | $other = []; |
|
521 | 49 | $stack = []; |
|
522 | |||
523 | 49 | foreach ($props as $prop) { |
|
524 | 49 | switch ($prop[0]) { |
|
525 | 49 | case "comment": |
|
526 | 1 | $stack[] = $prop; |
|
527 | 1 | break; |
|
528 | 49 | case "assign": |
|
529 | 43 | $stack[] = $prop; |
|
530 | 43 | if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) { |
|
531 | 23 | $vars = array_merge($vars, $stack); |
|
532 | } else { |
||
533 | 43 | $other = array_merge($other, $stack); |
|
534 | } |
||
535 | 43 | $stack = []; |
|
536 | 43 | break; |
|
537 | 47 | case "import": |
|
538 | 3 | $id = self::$nextImportId++; |
|
539 | 3 | $prop[] = $id; |
|
540 | 3 | $stack[] = $prop; |
|
541 | 3 | $imports = array_merge($imports, $stack); |
|
542 | 3 | $other[] = ["import_mixin", $id]; |
|
543 | 3 | $stack = []; |
|
544 | 3 | break; |
|
545 | default: |
||
546 | 46 | $stack[] = $prop; |
|
547 | 46 | $other = array_merge($other, $stack); |
|
548 | 46 | $stack = []; |
|
549 | 49 | break; |
|
550 | } |
||
551 | } |
||
552 | 49 | $other = array_merge($other, $stack); |
|
553 | |||
554 | 49 | if ($split) { |
|
555 | 2 | return [array_merge($imports, $vars), $other]; |
|
556 | } else { |
||
557 | 49 | return array_merge($imports, $vars, $other); |
|
558 | } |
||
559 | } |
||
560 | |||
561 | /** |
||
562 | * @param array $queries |
||
563 | * |
||
564 | * @return string |
||
565 | */ |
||
566 | 3 | protected function compileMediaQuery(array $queries) |
|
567 | { |
||
568 | 3 | $compiledQueries = []; |
|
569 | 3 | foreach ($queries as $query) { |
|
570 | 3 | $parts = []; |
|
571 | 3 | foreach ($query as $q) { |
|
572 | 3 | switch ($q[0]) { |
|
573 | 3 | case "mediaType": |
|
574 | 3 | $parts[] = implode(" ", array_slice($q, 1)); |
|
575 | 3 | break; |
|
576 | 1 | case "mediaExp": |
|
577 | 1 | if (isset($q[2])) { |
|
578 | 1 | $parts[] = "($q[1]: " . |
|
579 | 1 | $this->compileValue($this->reduce($q[2])) . ")"; |
|
580 | } else { |
||
581 | 1 | $parts[] = "($q[1])"; |
|
582 | } |
||
583 | 1 | break; |
|
584 | 1 | case "variable": |
|
585 | 1 | $parts[] = $this->compileValue($this->reduce($q)); |
|
586 | 3 | break; |
|
587 | } |
||
588 | } |
||
589 | |||
590 | 3 | if (count($parts) > 0) { |
|
591 | 3 | $compiledQueries[] = implode(" and ", $parts); |
|
592 | } |
||
593 | } |
||
594 | |||
595 | 3 | $out = "@media"; |
|
596 | 3 | if (!empty($parts)) { |
|
597 | $out .= " " . |
||
598 | 3 | implode($this->formatter->getSelectorSeparator(), $compiledQueries); |
|
599 | } |
||
600 | |||
601 | 3 | return $out; |
|
602 | } |
||
603 | |||
604 | /** |
||
605 | * @param \LesserPhp\NodeEnv $env |
||
606 | * @param array $childQueries |
||
607 | * |
||
608 | * @return array |
||
609 | */ |
||
610 | 3 | protected function multiplyMedia(NodeEnv $env = null, array $childQueries = null) |
|
611 | { |
||
612 | 3 | if ($env === null || |
|
613 | 3 | (!empty($env->getBlock()->type) && $env->getBlock()->type !== 'media') |
|
614 | ) { |
||
615 | 3 | return $childQueries; |
|
616 | } |
||
617 | |||
618 | // plain old block, skip |
||
619 | 3 | if (empty($env->getBlock()->type)) { |
|
620 | 3 | return $this->multiplyMedia($env->getParent(), $childQueries); |
|
621 | } |
||
622 | |||
623 | 3 | $out = []; |
|
624 | 3 | $queries = $env->getBlock()->queries; |
|
625 | 3 | if ($childQueries === null) { |
|
626 | 3 | $out = $queries; |
|
627 | } else { |
||
628 | 1 | foreach ($queries as $parent) { |
|
629 | 1 | foreach ($childQueries as $child) { |
|
630 | 1 | $out[] = array_merge($parent, $child); |
|
631 | } |
||
632 | } |
||
633 | } |
||
634 | |||
635 | 3 | return $this->multiplyMedia($env->getParent(), $out); |
|
636 | } |
||
637 | |||
638 | /** |
||
639 | * @param $tag |
||
640 | * @param $replace |
||
641 | * |
||
642 | * @return int |
||
643 | */ |
||
644 | 46 | protected function expandParentSelectors(&$tag, $replace) |
|
645 | { |
||
646 | 46 | $parts = explode("$&$", $tag); |
|
647 | 46 | $count = 0; |
|
648 | 46 | foreach ($parts as &$part) { |
|
649 | 46 | $part = str_replace($this->parentSelector, $replace, $part, $c); |
|
650 | 46 | $count += $c; |
|
651 | } |
||
652 | 46 | $tag = implode($this->parentSelector, $parts); |
|
653 | |||
654 | 46 | return $count; |
|
655 | } |
||
656 | |||
657 | /** |
||
658 | * @return array|null |
||
659 | */ |
||
660 | 46 | protected function findClosestSelectors() |
|
661 | { |
||
662 | 46 | $env = $this->env; |
|
663 | 46 | $selectors = null; |
|
664 | 46 | while ($env !== null) { |
|
665 | 46 | if ($env->getSelectors() !== null) { |
|
666 | 13 | $selectors = $env->getSelectors(); |
|
667 | 13 | break; |
|
668 | } |
||
669 | 46 | $env = $env->getParent(); |
|
670 | } |
||
671 | |||
672 | 46 | return $selectors; |
|
673 | } |
||
674 | |||
675 | |||
676 | /** |
||
677 | * multiply $selectors against the nearest selectors in env |
||
678 | * |
||
679 | * @param array $selectors |
||
680 | * |
||
681 | * @return array |
||
682 | */ |
||
683 | 46 | protected function multiplySelectors(array $selectors) |
|
684 | { |
||
685 | // find parent selectors |
||
686 | |||
687 | 46 | $parentSelectors = $this->findClosestSelectors(); |
|
688 | 46 | if ($parentSelectors === null) { |
|
689 | // kill parent reference in top level selector |
||
690 | 46 | foreach ($selectors as &$s) { |
|
691 | 46 | $this->expandParentSelectors($s, ""); |
|
692 | } |
||
693 | |||
694 | 46 | return $selectors; |
|
695 | } |
||
696 | |||
697 | 13 | $out = []; |
|
698 | 13 | foreach ($parentSelectors as $parent) { |
|
699 | 13 | foreach ($selectors as $child) { |
|
700 | 13 | $count = $this->expandParentSelectors($child, $parent); |
|
701 | |||
702 | // don't prepend the parent tag if & was used |
||
703 | 13 | if ($count > 0) { |
|
704 | 4 | $out[] = trim($child); |
|
705 | } else { |
||
706 | 13 | $out[] = trim($parent . ' ' . $child); |
|
707 | } |
||
708 | } |
||
709 | } |
||
710 | |||
711 | 13 | return $out; |
|
712 | } |
||
713 | |||
714 | /** |
||
715 | * reduces selector expressions |
||
716 | * |
||
717 | * @param array $selectors |
||
718 | * |
||
719 | * @return array |
||
720 | * @throws \LesserPhp\Exception\GeneralException |
||
721 | */ |
||
722 | 46 | protected function compileSelectors(array $selectors) |
|
723 | { |
||
724 | 46 | $out = []; |
|
725 | |||
726 | 46 | foreach ($selectors as $s) { |
|
727 | 46 | if (is_array($s)) { |
|
728 | 4 | list(, $value) = $s; |
|
729 | 4 | $out[] = trim($this->compileValue($this->reduce($value))); |
|
730 | } else { |
||
731 | 46 | $out[] = $s; |
|
732 | } |
||
733 | } |
||
734 | |||
735 | 46 | return $out; |
|
736 | } |
||
737 | |||
738 | /** |
||
739 | * @param $left |
||
740 | * @param $right |
||
741 | * |
||
742 | * @return bool |
||
743 | */ |
||
744 | 4 | protected function equals($left, $right) |
|
745 | { |
||
746 | 4 | return $left == $right; |
|
747 | } |
||
748 | |||
749 | /** |
||
750 | * @param $block |
||
751 | * @param $orderedArgs |
||
752 | * @param $keywordArgs |
||
753 | * |
||
754 | * @return bool |
||
755 | */ |
||
756 | 21 | protected function patternMatch($block, $orderedArgs, $keywordArgs) |
|
757 | { |
||
758 | // match the guards if it has them |
||
759 | // any one of the groups must have all its guards pass for a match |
||
760 | 21 | if (!empty($block->guards)) { |
|
761 | 5 | $groupPassed = false; |
|
762 | 5 | foreach ($block->guards as $guardGroup) { |
|
763 | 5 | foreach ($guardGroup as $guard) { |
|
764 | 5 | $this->pushEnv($this->env); |
|
765 | 5 | $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs); |
|
766 | |||
767 | 5 | $negate = false; |
|
768 | 5 | if ($guard[0] === "negate") { |
|
769 | 1 | $guard = $guard[1]; |
|
770 | 1 | $negate = true; |
|
771 | } |
||
772 | |||
773 | 5 | $passed = $this->reduce($guard) == self::$TRUE; |
|
774 | 5 | if ($negate) { |
|
775 | 1 | $passed = !$passed; |
|
776 | } |
||
777 | |||
778 | 5 | $this->popEnv(); |
|
779 | |||
780 | 5 | if ($passed) { |
|
781 | 3 | $groupPassed = true; |
|
782 | } else { |
||
783 | 5 | $groupPassed = false; |
|
784 | 5 | break; |
|
785 | } |
||
786 | } |
||
787 | |||
788 | 5 | if ($groupPassed) { |
|
789 | 5 | break; |
|
790 | } |
||
791 | } |
||
792 | |||
793 | 5 | if (!$groupPassed) { |
|
794 | 5 | return false; |
|
795 | } |
||
796 | } |
||
797 | |||
798 | 19 | if (empty($block->args)) { |
|
799 | 12 | return $block->isVararg || empty($orderedArgs) && empty($keywordArgs); |
|
800 | } |
||
801 | |||
802 | 14 | $remainingArgs = $block->args; |
|
803 | 14 | if ($keywordArgs) { |
|
804 | 2 | $remainingArgs = []; |
|
805 | 2 | foreach ($block->args as $arg) { |
|
806 | 2 | if ($arg[0] === "arg" && isset($keywordArgs[$arg[1]])) { |
|
807 | 2 | continue; |
|
808 | } |
||
809 | |||
810 | 2 | $remainingArgs[] = $arg; |
|
811 | } |
||
812 | } |
||
813 | |||
814 | 14 | $i = -1; // no args |
|
815 | // try to match by arity or by argument literal |
||
816 | 14 | foreach ($remainingArgs as $i => $arg) { |
|
817 | 14 | switch ($arg[0]) { |
|
818 | 14 | case "lit": |
|
819 | 3 | if (empty($orderedArgs[$i]) || !$this->equals($arg[1], $orderedArgs[$i])) { |
|
820 | 2 | return false; |
|
821 | } |
||
822 | 3 | break; |
|
823 | 14 | case "arg": |
|
824 | // no arg and no default value |
||
825 | 14 | if (!isset($orderedArgs[$i]) && !isset($arg[2])) { |
|
826 | 3 | return false; |
|
827 | } |
||
828 | 14 | break; |
|
829 | 2 | case "rest": |
|
830 | 2 | $i--; // rest can be empty |
|
831 | 14 | break 2; |
|
832 | } |
||
833 | } |
||
834 | |||
835 | 13 | if ($block->isVararg) { |
|
836 | 2 | return true; // not having enough is handled above |
|
837 | } else { |
||
838 | 13 | $numMatched = $i + 1; |
|
839 | |||
840 | // greater than because default values always match |
||
841 | 13 | return $numMatched >= count($orderedArgs); |
|
842 | } |
||
843 | } |
||
844 | |||
845 | /** |
||
846 | * @param array $blocks |
||
847 | * @param $orderedArgs |
||
848 | * @param $keywordArgs |
||
849 | * @param array $skip |
||
850 | * |
||
851 | * @return array|null |
||
852 | */ |
||
853 | 21 | protected function patternMatchAll(array $blocks, $orderedArgs, $keywordArgs, array $skip = []) |
|
854 | { |
||
855 | 21 | $matches = null; |
|
856 | 21 | foreach ($blocks as $block) { |
|
857 | // skip seen blocks that don't have arguments |
||
858 | 21 | if (isset($skip[$block->id]) && !isset($block->args)) { |
|
859 | 1 | continue; |
|
860 | } |
||
861 | |||
862 | 21 | if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) { |
|
863 | 21 | $matches[] = $block; |
|
864 | } |
||
865 | } |
||
866 | |||
867 | 21 | return $matches; |
|
868 | } |
||
869 | |||
870 | /** |
||
871 | * attempt to find blocks matched by path and args |
||
872 | * |
||
873 | * @param $searchIn |
||
874 | * @param array $path |
||
875 | * @param $orderedArgs |
||
876 | * @param $keywordArgs |
||
877 | * @param array $seen |
||
878 | * |
||
879 | * @return array|null |
||
880 | */ |
||
881 | 22 | protected function findBlocks($searchIn, array $path, $orderedArgs, $keywordArgs, array $seen = []) |
|
882 | { |
||
883 | 22 | if ($searchIn === null) { |
|
884 | 5 | return null; |
|
885 | } |
||
886 | 22 | if (isset($seen[$searchIn->id])) { |
|
887 | 1 | return null; |
|
888 | } |
||
889 | 22 | $seen[$searchIn->id] = true; |
|
890 | |||
891 | 22 | $name = $path[0]; |
|
892 | |||
893 | 22 | if (isset($searchIn->children[$name])) { |
|
894 | 21 | $blocks = $searchIn->children[$name]; |
|
895 | 21 | if (count($path) === 1) { |
|
896 | 21 | $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen); |
|
897 | 21 | if (!empty($matches)) { |
|
898 | // This will return all blocks that match in the closest |
||
899 | // scope that has any matching block, like lessjs |
||
900 | 21 | return $matches; |
|
901 | } |
||
902 | } else { |
||
903 | 3 | $matches = []; |
|
904 | 3 | foreach ($blocks as $subBlock) { |
|
905 | 3 | $subMatches = $this->findBlocks( |
|
906 | $subBlock, |
||
907 | 3 | array_slice($path, 1), |
|
908 | $orderedArgs, |
||
909 | $keywordArgs, |
||
910 | $seen |
||
911 | ); |
||
912 | |||
913 | 3 | if ($subMatches !== null) { |
|
914 | 3 | foreach ($subMatches as $sm) { |
|
915 | 3 | $matches[] = $sm; |
|
916 | } |
||
917 | } |
||
918 | } |
||
919 | |||
920 | 3 | return count($matches) > 0 ? $matches : null; |
|
921 | } |
||
922 | } |
||
923 | 22 | if ($searchIn->parent === $searchIn) { |
|
924 | return null; |
||
925 | } |
||
926 | |||
927 | 22 | return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen); |
|
928 | } |
||
929 | |||
930 | /** |
||
931 | * sets all argument names in $args to either the default value |
||
932 | * or the one passed in through $values |
||
933 | * |
||
934 | * @param array $args |
||
935 | * @param $orderedValues |
||
936 | * @param $keywordValues |
||
937 | * |
||
938 | * @throws \LesserPhp\Exception\GeneralException |
||
939 | */ |
||
940 | 16 | protected function zipSetArgs(array $args, $orderedValues, $keywordValues) |
|
941 | { |
||
942 | 16 | $assignedValues = []; |
|
943 | |||
944 | 16 | $i = 0; |
|
945 | 16 | foreach ($args as $a) { |
|
946 | 14 | if ($a[0] === "arg") { |
|
947 | 14 | if (isset($keywordValues[$a[1]])) { |
|
948 | // has keyword arg |
||
949 | 2 | $value = $keywordValues[$a[1]]; |
|
950 | 14 | } elseif (isset($orderedValues[$i])) { |
|
951 | // has ordered arg |
||
952 | 13 | $value = $orderedValues[$i]; |
|
953 | 13 | $i++; |
|
954 | 6 | } elseif (isset($a[2])) { |
|
955 | // has default value |
||
956 | 6 | $value = $a[2]; |
|
957 | } else { |
||
958 | throw new GeneralException('Failed to assign arg ' . $a[1]); |
||
959 | } |
||
960 | |||
961 | 14 | $value = $this->reduce($value); |
|
962 | 14 | $this->set($a[1], $value); |
|
963 | 14 | $assignedValues[] = $value; |
|
964 | } else { |
||
965 | // a lit |
||
966 | 14 | $i++; |
|
967 | } |
||
968 | } |
||
969 | |||
970 | // check for a rest |
||
971 | 16 | $last = end($args); |
|
972 | 16 | if ($last[0] === "rest") { |
|
973 | 2 | $rest = array_slice($orderedValues, count($args) - 1); |
|
974 | 2 | $this->set($last[1], $this->reduce(["list", " ", $rest])); |
|
975 | } |
||
976 | |||
977 | // wow is this the only true use of PHP's + operator for arrays? |
||
978 | 16 | $this->env->setArguments($assignedValues + $orderedValues); |
|
979 | 16 | } |
|
980 | |||
981 | /** |
||
982 | * compile a prop and update $lines or $blocks appropriately |
||
983 | * |
||
984 | * @param $prop |
||
985 | * @param $block |
||
986 | * @param $out |
||
987 | * |
||
988 | * @throws \LesserPhp\Exception\GeneralException |
||
989 | */ |
||
990 | 49 | protected function compileProp($prop, $block, $out) |
|
991 | { |
||
992 | // set error position context |
||
993 | 49 | $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1; |
|
994 | |||
995 | 49 | switch ($prop[0]) { |
|
996 | 49 | case 'assign': |
|
997 | 43 | list(, $name, $value) = $prop; |
|
998 | 43 | if ($name[0] == $this->vPrefix) { |
|
999 | 23 | $this->set($name, $value); |
|
1000 | } else { |
||
1001 | 43 | $out->lines[] = $this->formatter->property( |
|
1002 | $name, |
||
1003 | 43 | $this->compileValue($this->reduce($value)) |
|
1004 | ); |
||
1005 | } |
||
1006 | 37 | break; |
|
1007 | 47 | case 'block': |
|
1008 | 46 | list(, $child) = $prop; |
|
1009 | 46 | $this->compileBlock($child); |
|
1010 | 35 | break; |
|
1011 | 25 | case 'ruleset': |
|
1012 | 25 | case 'mixin': |
|
1013 | 22 | list(, $path, $args, $suffix) = $prop; |
|
1014 | |||
1015 | 22 | $orderedArgs = []; |
|
1016 | 22 | $keywordArgs = []; |
|
1017 | 22 | foreach ((array)$args as $arg) { |
|
1018 | 15 | switch ($arg[0]) { |
|
1019 | 15 | case "arg": |
|
1020 | 4 | if (!isset($arg[2])) { |
|
1021 | 3 | $orderedArgs[] = $this->reduce(["variable", $arg[1]]); |
|
1022 | } else { |
||
1023 | 2 | $keywordArgs[$arg[1]] = $this->reduce($arg[2]); |
|
1024 | } |
||
1025 | 4 | break; |
|
1026 | |||
1027 | 15 | case "lit": |
|
1028 | 15 | $orderedArgs[] = $this->reduce($arg[1]); |
|
1029 | 15 | break; |
|
1030 | default: |
||
1031 | 15 | throw new GeneralException("Unknown arg type: " . $arg[0]); |
|
1032 | } |
||
1033 | } |
||
1034 | |||
1035 | 22 | $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs); |
|
1036 | |||
1037 | 22 | if ($mixins === null) { |
|
1038 | 5 | $block->parser->throwError("{$prop[1][0]} is undefined", $block->count); |
|
1039 | } |
||
1040 | |||
1041 | 17 | if (strpos($prop[1][0], "$") === 0) { |
|
1042 | //Use Ruleset Logic - Only last element |
||
1043 | 8 | $mixins = [array_pop($mixins)]; |
|
1044 | } |
||
1045 | |||
1046 | 17 | foreach ($mixins as $mixin) { |
|
0 ignored issues
–
show
|
|||
1047 | 17 | if ($mixin === $block && !$orderedArgs) { |
|
1048 | continue; |
||
1049 | } |
||
1050 | |||
1051 | 17 | $haveScope = false; |
|
1052 | 17 | if (isset($mixin->parent->scope)) { |
|
1053 | 2 | $haveScope = true; |
|
1054 | 2 | $mixinParentEnv = $this->pushEnv($this->env); |
|
1055 | 2 | $mixinParentEnv->storeParent = $mixin->parent->scope; |
|
1056 | } |
||
1057 | |||
1058 | 17 | $haveArgs = false; |
|
1059 | 17 | if (isset($mixin->args)) { |
|
1060 | 14 | $haveArgs = true; |
|
1061 | 14 | $this->pushEnv($this->env); |
|
1062 | 14 | $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs); |
|
1063 | } |
||
1064 | |||
1065 | 17 | $oldParent = $mixin->parent; |
|
1066 | 17 | if ($mixin != $block) { |
|
1067 | 17 | $mixin->parent = $block; |
|
1068 | } |
||
1069 | |||
1070 | 17 | foreach ($this->sortProps($mixin->props) as $subProp) { |
|
1071 | 17 | if ($suffix !== null && |
|
1072 | 17 | $subProp[0] === "assign" && |
|
1073 | 17 | is_string($subProp[1]) && |
|
1074 | 17 | $subProp[1]{0} != $this->vPrefix |
|
1075 | ) { |
||
1076 | 1 | $subProp[2] = [ |
|
1077 | 1 | 'list', |
|
1078 | 1 | ' ', |
|
1079 | 1 | [$subProp[2], ['keyword', $suffix]], |
|
1080 | ]; |
||
1081 | } |
||
1082 | |||
1083 | 17 | $this->compileProp($subProp, $mixin, $out); |
|
1084 | } |
||
1085 | |||
1086 | 17 | $mixin->parent = $oldParent; |
|
1087 | |||
1088 | 17 | if ($haveArgs) { |
|
1089 | 14 | $this->popEnv(); |
|
1090 | } |
||
1091 | 17 | if ($haveScope) { |
|
1092 | 17 | $this->popEnv(); |
|
1093 | } |
||
1094 | } |
||
1095 | |||
1096 | 17 | break; |
|
1097 | 5 | case 'raw': |
|
1098 | $out->lines[] = $prop[1]; |
||
1099 | break; |
||
1100 | 5 | case "directive": |
|
1101 | 1 | list(, $name, $value) = $prop; |
|
1102 | 1 | $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)) . ';'; |
|
1103 | 1 | break; |
|
1104 | 4 | case "comment": |
|
1105 | 1 | $out->lines[] = $prop[1]; |
|
1106 | 1 | break; |
|
1107 | 3 | case "import": |
|
1108 | 3 | list(, $importPath, $importId) = $prop; |
|
1109 | 3 | $importPath = $this->reduce($importPath); |
|
1110 | |||
1111 | 3 | $result = $this->tryImport($importPath, $block, $out); |
|
1112 | |||
1113 | 3 | $this->env->addImports($importId, $result === false ? |
|
1114 | 2 | [false, "@import " . $this->compileValue($importPath) . ";"] : |
|
1115 | 3 | $result); |
|
1116 | |||
1117 | 3 | break; |
|
1118 | 3 | case "import_mixin": |
|
1119 | 3 | list(, $importId) = $prop; |
|
1120 | 3 | $import = $this->env->getImports($importId); |
|
1121 | 3 | if ($import[0] === false) { |
|
1122 | 3 | if (isset($import[1])) { |
|
1123 | 3 | $out->lines[] = $import[1]; |
|
1124 | } |
||
1125 | } else { |
||
1126 | 2 | list(, $bottom, $parser, $importDir) = $import; |
|
1127 | 2 | $this->compileImportedProps($bottom, $block, $out, $importDir); |
|
1128 | } |
||
1129 | |||
1130 | 3 | break; |
|
1131 | default: |
||
1132 | $block->parser->throwError("unknown op: {$prop[0]}\n", $block->count); |
||
1133 | } |
||
1134 | 38 | } |
|
1135 | |||
1136 | |||
1137 | /** |
||
1138 | * Compiles a primitive value into a CSS property value. |
||
1139 | * |
||
1140 | * Values in lessphp are typed by being wrapped in arrays, their format is |
||
1141 | * typically: |
||
1142 | * |
||
1143 | * array(type, contents [, additional_contents]*) |
||
1144 | * |
||
1145 | * The input is expected to be reduced. This function will not work on |
||
1146 | * things like expressions and variables. |
||
1147 | * |
||
1148 | * @param array $value |
||
1149 | * |
||
1150 | * @return string |
||
1151 | * @throws \LesserPhp\Exception\GeneralException |
||
1152 | */ |
||
1153 | 38 | public function compileValue(array $value) |
|
1154 | { |
||
1155 | 38 | switch ($value[0]) { |
|
1156 | 38 | case 'list': |
|
1157 | // [1] - delimiter |
||
1158 | // [2] - array of values |
||
1159 | 21 | return implode($value[1], array_map([$this, 'compileValue'], $value[2])); |
|
1160 | 38 | case 'raw_color': |
|
1161 | 8 | if ($this->formatter->getCompressColors()) { |
|
1162 | return $this->compileValue($this->coerce->coerceColor($value)); |
||
1163 | } |
||
1164 | |||
1165 | 8 | return $value[1]; |
|
1166 | 38 | case 'keyword': |
|
1167 | // [1] - the keyword |
||
1168 | 32 | return $value[1]; |
|
1169 | 36 | case 'number': |
|
1170 | 32 | list(, $num, $unit) = $value; |
|
1171 | // [1] - the number |
||
1172 | // [2] - the unit |
||
1173 | 32 | if ($this->numberPrecision !== null) { |
|
1174 | $num = round($num, $this->numberPrecision); |
||
1175 | } |
||
1176 | |||
1177 | 32 | return $num . $unit; |
|
1178 | 29 | case 'string': |
|
1179 | // [1] - contents of string (includes quotes) |
||
1180 | 24 | list(, $delim, $content) = $value; |
|
1181 | 24 | foreach ($content as &$part) { |
|
1182 | 24 | if (is_array($part)) { |
|
1183 | 24 | $part = $this->compileValue($part); |
|
1184 | } |
||
1185 | } |
||
1186 | |||
1187 | 24 | return $delim . implode($content) . $delim; |
|
1188 | 17 | case 'color': |
|
1189 | // [1] - red component (either number or a %) |
||
1190 | // [2] - green component |
||
1191 | // [3] - blue component |
||
1192 | // [4] - optional alpha component |
||
1193 | 8 | list(, $r, $g, $b) = $value; |
|
1194 | 8 | $r = round($r); |
|
1195 | 8 | $g = round($g); |
|
1196 | 8 | $b = round($b); |
|
1197 | |||
1198 | 8 | if (count($value) === 5 && $value[4] != 1) { // rgba |
|
1199 | 3 | return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $value[4] . ')'; |
|
1200 | } |
||
1201 | |||
1202 | 8 | $h = sprintf("#%02x%02x%02x", $r, $g, $b); |
|
1203 | |||
1204 | 8 | if ($this->formatter->getCompressColors()) { |
|
1205 | // Converting hex color to short notation (e.g. #003399 to #039) |
||
1206 | 1 | if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) { |
|
1207 | 1 | $h = '#' . $h[1] . $h[3] . $h[5]; |
|
1208 | } |
||
1209 | } |
||
1210 | |||
1211 | 8 | return $h; |
|
1212 | |||
1213 | 10 | case 'function': |
|
1214 | 10 | list(, $name, $args) = $value; |
|
1215 | |||
1216 | 10 | return $name . '(' . $this->compileValue($args) . ')'; |
|
1217 | default: // assumed to be unit |
||
1218 | throw new GeneralException('unknown value type: ' . $value[0]); |
||
1219 | } |
||
1220 | } |
||
1221 | |||
1222 | /** |
||
1223 | * Helper function to get arguments for color manipulation functions. |
||
1224 | * takes a list that contains a color like thing and a percentage |
||
1225 | * |
||
1226 | * @param array $args |
||
1227 | * |
||
1228 | * @return array |
||
1229 | */ |
||
1230 | 2 | public function colorArgs(array $args) |
|
1231 | { |
||
1232 | 2 | if ($args[0] !== 'list' || count($args[2]) < 2) { |
|
1233 | 1 | return [['color', 0, 0, 0], 0]; |
|
1234 | } |
||
1235 | 2 | list($color, $delta) = $args[2]; |
|
1236 | 2 | $color = $this->assertions->assertColor($color); |
|
1237 | 2 | $delta = (float)$delta[1]; |
|
1238 | |||
1239 | 2 | return [$color, $delta]; |
|
1240 | } |
||
1241 | |||
1242 | /** |
||
1243 | * Convert the rgb, rgba, hsl color literals of function type |
||
1244 | * as returned by the parser into values of color type. |
||
1245 | * |
||
1246 | * @param array $func |
||
1247 | * |
||
1248 | * @return bool|mixed |
||
1249 | */ |
||
1250 | 24 | protected function funcToColor(array $func) |
|
1251 | { |
||
1252 | 24 | $fname = $func[1]; |
|
1253 | 24 | if ($func[2][0] !== 'list') { |
|
1254 | 6 | return false; |
|
1255 | } // need a list of arguments |
||
1256 | /** @var array $rawComponents */ |
||
1257 | 24 | $rawComponents = $func[2][2]; |
|
1258 | |||
1259 | 24 | if ($fname === 'hsl' || $fname === 'hsla') { |
|
1260 | 1 | $hsl = ['hsl']; |
|
1261 | 1 | $i = 0; |
|
1262 | 1 | foreach ($rawComponents as $c) { |
|
1263 | 1 | $val = $this->reduce($c); |
|
1264 | 1 | $val = isset($val[1]) ? (float)$val[1] : 0; |
|
1265 | |||
1266 | 1 | if ($i === 0) { |
|
1267 | 1 | $clamp = 360; |
|
1268 | 1 | } elseif ($i < 3) { |
|
1269 | 1 | $clamp = 100; |
|
1270 | } else { |
||
1271 | 1 | $clamp = 1; |
|
1272 | } |
||
1273 | |||
1274 | 1 | $hsl[] = $this->converter->clamp($val, $clamp); |
|
1275 | 1 | $i++; |
|
1276 | } |
||
1277 | |||
1278 | 1 | while (count($hsl) < 4) { |
|
1279 | $hsl[] = 0; |
||
1280 | } |
||
1281 | |||
1282 | 1 | return $this->converter->toRGB($hsl); |
|
1283 | |||
1284 | 24 | } elseif ($fname === 'rgb' || $fname === 'rgba') { |
|
1285 | 4 | $components = []; |
|
1286 | 4 | $i = 1; |
|
1287 | 4 | foreach ($rawComponents as $c) { |
|
1288 | 4 | $c = $this->reduce($c); |
|
1289 | 4 | if ($i < 4) { |
|
1290 | 4 | View Code Duplication | if ($c[0] === "number" && $c[2] === "%") { |
1291 | 1 | $components[] = 255 * ($c[1] / 100); |
|
1292 | } else { |
||
1293 | 4 | $components[] = (float)$c[1]; |
|
1294 | } |
||
1295 | 4 | View Code Duplication | } elseif ($i === 4) { |
1296 | 4 | if ($c[0] === "number" && $c[2] === "%") { |
|
1297 | $components[] = 1.0 * ($c[1] / 100); |
||
1298 | } else { |
||
1299 | 4 | $components[] = (float)$c[1]; |
|
1300 | } |
||
1301 | } else { |
||
1302 | break; |
||
1303 | } |
||
1304 | |||
1305 | 4 | $i++; |
|
1306 | } |
||
1307 | 4 | while (count($components) < 3) { |
|
1308 | $components[] = 0; |
||
1309 | } |
||
1310 | 4 | array_unshift($components, 'color'); |
|
1311 | |||
1312 | 4 | return $this->fixColor($components); |
|
1313 | } |
||
1314 | |||
1315 | 23 | return false; |
|
1316 | } |
||
1317 | |||
1318 | /** |
||
1319 | * @param array $value |
||
1320 | * @param bool $forExpression |
||
1321 | * |
||
1322 | * @return array|bool|mixed|null // <!-- dafuq? |
||
1323 | */ |
||
1324 | 48 | public function reduce(array $value, $forExpression = false) |
|
1325 | { |
||
1326 | 48 | switch ($value[0]) { |
|
1327 | 48 | case "interpolate": |
|
1328 | 7 | $reduced = $this->reduce($value[1]); |
|
1329 | 7 | $var = $this->compileValue($reduced); |
|
1330 | 7 | $res = $this->reduce(["variable", $this->vPrefix . $var]); |
|
1331 | |||
1332 | 7 | if ($res[0] === "raw_color") { |
|
1333 | 1 | $res = $this->coerce->coerceColor($res); |
|
1334 | } |
||
1335 | |||
1336 | 7 | if (empty($value[2])) { |
|
1337 | 6 | $res = $this->functions->e($res); |
|
1338 | } |
||
1339 | |||
1340 | 7 | return $res; |
|
1341 | 48 | case "variable": |
|
1342 | 28 | $key = $value[1]; |
|
1343 | 28 | if (is_array($key)) { |
|
1344 | 1 | $key = $this->reduce($key); |
|
1345 | 1 | $key = $this->vPrefix . $this->compileValue($this->functions->e($key)); |
|
1346 | } |
||
1347 | |||
1348 | 28 | $seen =& $this->env->seenNames; |
|
1349 | |||
1350 | 28 | if (!empty($seen[$key])) { |
|
1351 | $this->throwError("infinite loop detected: $key"); |
||
1352 | } |
||
1353 | |||
1354 | 28 | $seen[$key] = true; |
|
1355 | 28 | $out = $this->reduce($this->get($key)); |
|
1356 | 27 | $seen[$key] = false; |
|
1357 | |||
1358 | 27 | return $out; |
|
1359 | 47 | case "list": |
|
1360 | 29 | foreach ($value[2] as &$item) { |
|
1361 | 26 | $item = $this->reduce($item, $forExpression); |
|
1362 | } |
||
1363 | |||
1364 | 29 | return $value; |
|
1365 | 47 | case "expression": |
|
1366 | 19 | return $this->evaluate($value); |
|
1367 | 47 | case "string": |
|
1368 | 26 | foreach ($value[2] as &$part) { |
|
1369 | 26 | if (is_array($part)) { |
|
1370 | 11 | $strip = $part[0] === "variable"; |
|
1371 | 11 | $part = $this->reduce($part); |
|
1372 | 11 | if ($strip) { |
|
1373 | 26 | $part = $this->functions->e($part); |
|
1374 | } |
||
1375 | } |
||
1376 | } |
||
1377 | |||
1378 | 26 | return $value; |
|
1379 | 45 | case "escape": |
|
1380 | 4 | list(, $inner) = $value; |
|
1381 | |||
1382 | 4 | return $this->functions->e($this->reduce($inner)); |
|
1383 | 45 | case "function": |
|
1384 | 24 | $color = $this->funcToColor($value); |
|
1385 | 24 | if ($color) { |
|
1386 | 4 | return $color; |
|
1387 | } |
||
1388 | |||
1389 | 23 | list(, $name, $args) = $value; |
|
1390 | 23 | if ($name === "%") { |
|
1391 | 1 | $name = "_sprintf"; |
|
1392 | } |
||
1393 | |||
1394 | // user functions |
||
1395 | 23 | $f = null; |
|
1396 | 23 | if (isset($this->libFunctions[$name]) && is_callable($this->libFunctions[$name])) { |
|
1397 | 1 | $f = $this->libFunctions[$name]; |
|
1398 | } |
||
1399 | |||
1400 | 23 | $func = str_replace('-', '_', $name); |
|
1401 | |||
1402 | 23 | if ($f !== null || method_exists($this->functions, $func)) { |
|
1403 | 14 | if ($args[0] === 'list') { |
|
1404 | 14 | $args = self::compressList($args[2], $args[1]); |
|
1405 | } |
||
1406 | |||
1407 | 14 | if ($f !== null) { |
|
1408 | 1 | $ret = $f($this->reduce($args, true), $this); |
|
1409 | } else { |
||
1410 | 13 | $ret = $this->functions->$func($this->reduce($args, true), $this); |
|
1411 | } |
||
1412 | 9 | if ($ret === null) { |
|
1413 | return [ |
||
1414 | 2 | "string", |
|
1415 | 2 | "", |
|
1416 | [ |
||
1417 | 2 | $name, |
|
1418 | 2 | "(", |
|
1419 | 2 | $args, |
|
1420 | 2 | ")", |
|
1421 | ], |
||
1422 | ]; |
||
1423 | } |
||
1424 | |||
1425 | // convert to a typed value if the result is a php primitive |
||
1426 | 8 | if (is_numeric($ret)) { |
|
1427 | 3 | $ret = ['number', $ret, ""]; |
|
1428 | 7 | } elseif (!is_array($ret)) { |
|
1429 | 2 | $ret = ['keyword', $ret]; |
|
1430 | } |
||
1431 | |||
1432 | 8 | return $ret; |
|
1433 | } |
||
1434 | |||
1435 | // plain function, reduce args |
||
1436 | 10 | $value[2] = $this->reduce($value[2]); |
|
1437 | |||
1438 | 10 | return $value; |
|
1439 | 40 | case "unary": |
|
1440 | 6 | list(, $op, $exp) = $value; |
|
1441 | 6 | $exp = $this->reduce($exp); |
|
1442 | |||
1443 | 6 | if ($exp[0] === "number") { |
|
1444 | switch ($op) { |
||
1445 | 6 | case "+": |
|
1446 | return $exp; |
||
1447 | 6 | case "-": |
|
1448 | 6 | $exp[1] *= -1; |
|
1449 | |||
1450 | 6 | return $exp; |
|
1451 | } |
||
1452 | } |
||
1453 | |||
1454 | return ["string", "", [$op, $exp]]; |
||
1455 | } |
||
1456 | |||
1457 | 40 | if ($forExpression) { |
|
1458 | 22 | switch ($value[0]) { |
|
1459 | 22 | case "keyword": |
|
1460 | 5 | $color = $this->coerce->coerceColor($value); |
|
1461 | 5 | if ($color !== null) { |
|
1462 | 2 | return $color; |
|
1463 | } |
||
1464 | 5 | break; |
|
1465 | 22 | case "raw_color": |
|
1466 | 6 | return $this->coerce->coerceColor($value); |
|
1467 | } |
||
1468 | } |
||
1469 | |||
1470 | 40 | return $value; |
|
1471 | } |
||
1472 | |||
1473 | /** |
||
1474 | * turn list of length 1 into value type |
||
1475 | * |
||
1476 | * @param array $value |
||
1477 | * |
||
1478 | * @return array |
||
1479 | */ |
||
1480 | 2 | protected function flattenList(array $value) |
|
1481 | { |
||
1482 | 2 | if ($value[0] === 'list' && count($value[2]) === 1) { |
|
1483 | 2 | return $this->flattenList($value[2][0]); |
|
1484 | } |
||
1485 | |||
1486 | 2 | return $value; |
|
1487 | } |
||
1488 | |||
1489 | /** |
||
1490 | * @param $a |
||
1491 | * |
||
1492 | * @return array |
||
1493 | */ |
||
1494 | 5 | public function toBool($a) |
|
1495 | { |
||
1496 | 5 | if ($a) { |
|
1497 | 4 | return self::$TRUE; |
|
1498 | } else { |
||
1499 | 5 | return self::$FALSE; |
|
1500 | } |
||
1501 | } |
||
1502 | |||
1503 | /** |
||
1504 | * evaluate an expression |
||
1505 | * |
||
1506 | * @param array $exp |
||
1507 | * |
||
1508 | * @return array |
||
1509 | */ |
||
1510 | 19 | protected function evaluate($exp) |
|
1511 | { |
||
1512 | 19 | list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp; |
|
1513 | |||
1514 | 19 | $left = $this->reduce($left, true); |
|
1515 | 19 | $right = $this->reduce($right, true); |
|
1516 | |||
1517 | 19 | $leftColor = $this->coerce->coerceColor($left); |
|
1518 | 19 | if ($leftColor !== null) { |
|
1519 | 5 | $left = $leftColor; |
|
1520 | } |
||
1521 | |||
1522 | 19 | $rightColor = $this->coerce->coerceColor($right); |
|
1523 | 19 | if ($rightColor !== null) { |
|
1524 | 5 | $right = $rightColor; |
|
1525 | } |
||
1526 | |||
1527 | 19 | $ltype = $left[0]; |
|
1528 | 19 | $rtype = $right[0]; |
|
1529 | |||
1530 | // operators that work on all types |
||
1531 | 19 | if ($op === "and") { |
|
1532 | return $this->toBool($left == self::$TRUE && $right == self::$TRUE); |
||
1533 | } |
||
1534 | |||
1535 | 19 | if ($op === "=") { |
|
1536 | 1 | return $this->toBool($this->equals($left, $right)); |
|
1537 | } |
||
1538 | |||
1539 | 19 | $str = $this->stringConcatenate($left, $right); |
|
1540 | 19 | if ($op === "+" && $str !== null) { |
|
1541 | 2 | return $str; |
|
1542 | } |
||
1543 | |||
1544 | // type based operators |
||
1545 | 17 | $fname = "op_${ltype}_${rtype}"; |
|
1546 | 17 | if (is_callable([$this, $fname])) { |
|
1547 | 17 | $out = $this->$fname($op, $left, $right); |
|
1548 | 17 | if ($out !== null) { |
|
1549 | 17 | return $out; |
|
1550 | } |
||
1551 | } |
||
1552 | |||
1553 | // make the expression look it did before being parsed |
||
1554 | 1 | $paddedOp = $op; |
|
1555 | 1 | if ($whiteBefore) { |
|
1556 | 1 | $paddedOp = " " . $paddedOp; |
|
1557 | } |
||
1558 | 1 | if ($whiteAfter) { |
|
1559 | 1 | $paddedOp .= " "; |
|
1560 | } |
||
1561 | |||
1562 | 1 | return ["string", "", [$left, $paddedOp, $right]]; |
|
1563 | } |
||
1564 | |||
1565 | /** |
||
1566 | * @param array $left |
||
1567 | * @param array $right |
||
1568 | * |
||
1569 | * @return array|null |
||
1570 | */ |
||
1571 | protected function stringConcatenate(array $left, array $right) |
||
1572 | { |
||
1573 | 19 | $strLeft = $this->coerce->coerceString($left); |
|
1574 | 19 | if ($strLeft !== null) { |
|
1575 | 2 | if ($right[0] === "string") { |
|
1576 | 2 | $right[1] = ""; |
|
1577 | } |
||
1578 | 2 | $strLeft[2][] = $right; |
|
1579 | |||
1580 | 2 | return $strLeft; |
|
1581 | } |
||
1582 | |||
1583 | 18 | $strRight = $this->coerce->coerceString($right); |
|
1584 | 18 | if ($strRight !== null) { |
|
1585 | 1 | array_unshift($strRight[2], $left); |
|
1586 | |||
1587 | 1 | return $strRight; |
|
1588 | } |
||
1589 | |||
1590 | 17 | return null; |
|
1591 | } |
||
1592 | |||
1593 | |||
1594 | /** |
||
1595 | * make sure a color's components don't go out of bounds |
||
1596 | * |
||
1597 | * @param array $c |
||
1598 | * |
||
1599 | * @return mixed |
||
1600 | */ |
||
1601 | public function fixColor(array $c) |
||
1602 | { |
||
1603 | 7 | foreach (range(1, 3) as $i) { |
|
1604 | 7 | if ($c[$i] < 0) { |
|
1605 | $c[$i] = 0; |
||
1606 | } |
||
1607 | 7 | if ($c[$i] > 255) { |
|
1608 | 7 | $c[$i] = 255; |
|
1609 | } |
||
1610 | } |
||
1611 | |||
1612 | 7 | return $c; |
|
1613 | } |
||
1614 | |||
1615 | /** |
||
1616 | * @param string $op |
||
1617 | * @param array $lft |
||
1618 | * @param array $rgt |
||
1619 | * |
||
1620 | * @return array|null |
||
1621 | * @throws \LesserPhp\Exception\GeneralException |
||
1622 | */ |
||
1623 | protected function op_number_color($op, array $lft, array $rgt) |
||
1624 | { |
||
1625 | 1 | if ($op === '+' || $op === '*') { |
|
1626 | 1 | return $this->op_color_number($op, $rgt, $lft); |
|
1627 | } |
||
1628 | |||
1629 | 1 | return null; |
|
1630 | } |
||
1631 | |||
1632 | /** |
||
1633 | * @param string $op |
||
1634 | * @param array $lft |
||
1635 | * @param array $rgt |
||
1636 | * |
||
1637 | * @return array |
||
1638 | * @throws \LesserPhp\Exception\GeneralException |
||
1639 | */ |
||
1640 | protected function op_color_number($op, array $lft, array $rgt) |
||
1641 | { |
||
1642 | 2 | if ($rgt[0] === '%') { |
|
1643 | $rgt[1] /= 100; |
||
1644 | } |
||
1645 | |||
1646 | 2 | return $this->op_color_color( |
|
1647 | $op, |
||
1648 | $lft, |
||
1649 | 2 | array_fill(1, count($lft) - 1, $rgt[1]) |
|
1650 | ); |
||
1651 | } |
||
1652 | |||
1653 | /** |
||
1654 | * @param string $op |
||
1655 | * @param array |
||
1656 | * $left |
||
1657 | * @param array $right |
||
1658 | * |
||
1659 | * @return array |
||
1660 | * @throws \LesserPhp\Exception\GeneralException |
||
1661 | */ |
||
1662 | protected function op_color_color($op, array $left, array $right) |
||
1663 | { |
||
1664 | 5 | $out = ['color']; |
|
1665 | 5 | $max = count($left) > count($right) ? count($left) : count($right); |
|
1666 | 5 | foreach (range(1, $max - 1) as $i) { |
|
1667 | 5 | $lval = isset($left[$i]) ? $left[$i] : 0; |
|
1668 | 5 | $rval = isset($right[$i]) ? $right[$i] : 0; |
|
1669 | switch ($op) { |
||
1670 | 5 | case '+': |
|
1671 | 5 | $out[] = $lval + $rval; |
|
1672 | 5 | break; |
|
1673 | 1 | case '-': |
|
1674 | 1 | $out[] = $lval - $rval; |
|
1675 | 1 | break; |
|
1676 | 1 | case '*': |
|
1677 | 1 | $out[] = $lval * $rval; |
|
1678 | 1 | break; |
|
1679 | 1 | case '%': |
|
1680 | 1 | $out[] = $lval % $rval; |
|
1681 | 1 | break; |
|
1682 | 1 | case '/': |
|
1683 | 1 | if ($rval == 0) { |
|
1684 | throw new GeneralException("evaluate error: can't divide by zero"); |
||
1685 | } |
||
1686 | 1 | $out[] = $lval / $rval; |
|
1687 | 1 | break; |
|
1688 | default: |
||
1689 | 5 | throw new GeneralException('evaluate error: color op number failed on op ' . $op); |
|
1690 | } |
||
1691 | } |
||
1692 | |||
1693 | 5 | return $this->fixColor($out); |
|
1694 | } |
||
1695 | |||
1696 | /** |
||
1697 | * operator on two numbers |
||
1698 | * |
||
1699 | * @param string $op |
||
1700 | * @param array $left |
||
1701 | * @param array $right |
||
1702 | * |
||
1703 | * @return array |
||
1704 | * @throws \LesserPhp\Exception\GeneralException |
||
1705 | */ |
||
1706 | protected function op_number_number($op, $left, $right) |
||
1707 | { |
||
1708 | 15 | $unit = empty($left[2]) ? $right[2] : $left[2]; |
|
1709 | |||
1710 | 15 | $value = 0; |
|
1711 | switch ($op) { |
||
1712 | 15 | case '+': |
|
1713 | 9 | $value = $left[1] + $right[1]; |
|
1714 | 9 | break; |
|
1715 | 12 | case '*': |
|
1716 | 8 | $value = $left[1] * $right[1]; |
|
1717 | 8 | break; |
|
1718 | 10 | case '-': |
|
1719 | 6 | $value = $left[1] - $right[1]; |
|
1720 | 6 | break; |
|
1721 | 9 | case '%': |
|
1722 | 1 | $value = $left[1] % $right[1]; |
|
1723 | 1 | break; |
|
1724 | 8 | case '/': |
|
1725 | 4 | if ($right[1] == 0) { |
|
1726 | throw new GeneralException('parse error: divide by zero'); |
||
1727 | } |
||
1728 | 4 | $value = $left[1] / $right[1]; |
|
1729 | 4 | break; |
|
1730 | 5 | case '<': |
|
1731 | 1 | return $this->toBool($left[1] < $right[1]); |
|
1732 | 5 | case '>': |
|
1733 | 3 | return $this->toBool($left[1] > $right[1]); |
|
1734 | 3 | case '>=': |
|
1735 | 1 | return $this->toBool($left[1] >= $right[1]); |
|
1736 | 2 | case '=<': |
|
1737 | 2 | return $this->toBool($left[1] <= $right[1]); |
|
1738 | default: |
||
1739 | throw new GeneralException('parse error: unknown number operator: ' . $op); |
||
1740 | } |
||
1741 | |||
1742 | 13 | return ['number', $value, $unit]; |
|
1743 | } |
||
1744 | |||
1745 | /** |
||
1746 | * @param $type |
||
1747 | * @param null $selectors |
||
1748 | * |
||
1749 | * @return \stdClass |
||
1750 | */ |
||
1751 | protected function makeOutputBlock($type, $selectors = null) |
||
1752 | { |
||
1753 | 49 | $b = new \stdClass(); |
|
1754 | 49 | $b->lines = []; |
|
1755 | 49 | $b->children = []; |
|
1756 | 49 | $b->selectors = $selectors; |
|
1757 | 49 | $b->type = $type; |
|
1758 | 49 | $b->parent = $this->scope; |
|
1759 | |||
1760 | 49 | return $b; |
|
1761 | } |
||
1762 | |||
1763 | /** |
||
1764 | * @param $parent |
||
1765 | * @param null $block |
||
1766 | * |
||
1767 | * @return \LesserPhp\NodeEnv |
||
1768 | */ |
||
1769 | protected function pushEnv($parent, $block = null) |
||
1770 | { |
||
1771 | 49 | $e = new \LesserPhp\NodeEnv(); |
|
1772 | 49 | $e->setParent($parent); |
|
1773 | 49 | $e->setBlock($block); |
|
1774 | 49 | $e->setStore([]); |
|
1775 | |||
1776 | 49 | $this->env = $e; |
|
1777 | |||
1778 | 49 | return $e; |
|
1779 | } |
||
1780 | |||
1781 | /** |
||
1782 | * pop something off the stack |
||
1783 | * |
||
1784 | * @return \LesserPhp\NodeEnv |
||
1785 | */ |
||
1786 | protected function popEnv() |
||
1787 | { |
||
1788 | 40 | $old = $this->env; |
|
1789 | 40 | $this->env = $this->env->getParent(); |
|
1790 | |||
1791 | 40 | return $old; |
|
1792 | } |
||
1793 | |||
1794 | /** |
||
1795 | * set something in the current env |
||
1796 | * |
||
1797 | * @param $name |
||
1798 | * @param $value |
||
1799 | */ |
||
1800 | protected function set($name, $value) |
||
1801 | { |
||
1802 | 27 | $this->env->addStore($name, $value); |
|
1803 | 27 | } |
|
1804 | |||
1805 | /** |
||
1806 | * get the highest occurrence entry for a name |
||
1807 | * |
||
1808 | * @param $name |
||
1809 | * |
||
1810 | * @return array |
||
1811 | * @throws \LesserPhp\Exception\GeneralException |
||
1812 | */ |
||
1813 | protected function get($name) |
||
1814 | { |
||
1815 | 28 | $current = $this->env; |
|
1816 | |||
1817 | // track scope to evaluate |
||
1818 | 28 | $scopeSecondary = []; |
|
1819 | |||
1820 | 28 | $isArguments = $name === $this->vPrefix . 'arguments'; |
|
1821 | 28 | while ($current) { |
|
1822 | 28 | if ($isArguments && count($current->getArguments()) > 0) { |
|
1823 | 3 | return ['list', ' ', $current->getArguments()]; |
|
1824 | } |
||
1825 | |||
1826 | 28 | if (isset($current->getStore()[$name])) { |
|
1827 | 27 | return $current->getStore()[$name]; |
|
1828 | } |
||
1829 | // has secondary scope? |
||
1830 | 19 | if (isset($current->storeParent)) { |
|
1831 | $scopeSecondary[] = $current->storeParent; |
||
1832 | } |
||
1833 | |||
1834 | 19 | if ($current->getParent() !== null) { |
|
1835 | 19 | $current = $current->getParent(); |
|
1836 | } else { |
||
1837 | 1 | $current = null; |
|
1838 | } |
||
1839 | } |
||
1840 | |||
1841 | 1 | while (count($scopeSecondary)) { |
|
1842 | // pop one off |
||
1843 | $current = array_shift($scopeSecondary); |
||
1844 | while ($current) { |
||
1845 | if ($isArguments && isset($current->arguments)) { |
||
1846 | return ['list', ' ', $current->arguments]; |
||
1847 | } |
||
1848 | |||
1849 | if (isset($current->store[$name])) { |
||
1850 | return $current->store[$name]; |
||
1851 | } |
||
1852 | |||
1853 | // has secondary scope? |
||
1854 | if (isset($current->storeParent)) { |
||
1855 | $scopeSecondary[] = $current->storeParent; |
||
1856 | } |
||
1857 | |||
1858 | if (isset($current->parent)) { |
||
1859 | $current = $current->parent; |
||
1860 | } else { |
||
1861 | $current = null; |
||
1862 | } |
||
1863 | } |
||
1864 | } |
||
1865 | |||
1866 | 1 | throw new GeneralException("variable $name is undefined"); |
|
1867 | } |
||
1868 | |||
1869 | /** |
||
1870 | * inject array of unparsed strings into environment as variables |
||
1871 | * |
||
1872 | * @param array $args |
||
1873 | * |
||
1874 | * @throws \LesserPhp\Exception\GeneralException |
||
1875 | */ |
||
1876 | protected function injectVariables(array $args) |
||
1877 | { |
||
1878 | 1 | $this->pushEnv($this->env); |
|
1879 | 1 | $parser = new Parser($this, __METHOD__); |
|
1880 | 1 | foreach ($args as $name => $strValue) { |
|
1881 | 1 | if ($name{0} !== '@') { |
|
1882 | 1 | $name = '@' . $name; |
|
1883 | } |
||
1884 | 1 | $parser->count = 0; |
|
1885 | 1 | $parser->buffer = (string)$strValue; |
|
1886 | 1 | if (!$parser->propertyValue($value)) { |
|
1887 | throw new GeneralException("failed to parse passed in variable $name: $strValue"); |
||
1888 | } |
||
1889 | |||
1890 | 1 | $this->set($name, $value); |
|
1891 | } |
||
1892 | 1 | } |
|
1893 | |||
1894 | /** |
||
1895 | * @param string $string |
||
1896 | * @param string $name |
||
1897 | * |
||
1898 | * @return string |
||
1899 | * @throws \LesserPhp\Exception\GeneralException |
||
1900 | */ |
||
1901 | public function compile($string, $name = null) |
||
1902 | { |
||
1903 | 49 | $locale = setlocale(LC_NUMERIC, 0); |
|
1904 | 49 | setlocale(LC_NUMERIC, 'C'); |
|
1905 | |||
1906 | 49 | $this->parser = $this->makeParser($name); |
|
1907 | 49 | $root = $this->parser->parse($string); |
|
1908 | |||
1909 | 49 | $this->env = null; |
|
1910 | 49 | $this->scope = null; |
|
1911 | 49 | $this->allParsedFiles = []; |
|
1912 | |||
1913 | 49 | $this->formatter = $this->newFormatter(); |
|
1914 | |||
1915 | 49 | if (!empty($this->registeredVars)) { |
|
1916 | 1 | $this->injectVariables($this->registeredVars); |
|
1917 | } |
||
1918 | |||
1919 | 49 | $this->sourceParser = $this->parser; // used for error messages |
|
1920 | 49 | $this->compileBlock($root); |
|
1921 | |||
1922 | 38 | ob_start(); |
|
1923 | 38 | $this->formatter->block($this->scope); |
|
1924 | 38 | $out = ob_get_clean(); |
|
1925 | 38 | setlocale(LC_NUMERIC, $locale); |
|
1926 | |||
1927 | 38 | return $out; |
|
1928 | } |
||
1929 | |||
1930 | /** |
||
1931 | * @param string $fname |
||
1932 | * @param string $outFname |
||
1933 | * |
||
1934 | * @return int|string |
||
1935 | * @throws \LesserPhp\Exception\GeneralException |
||
1936 | */ |
||
1937 | public function compileFile($fname, $outFname = null) |
||
1938 | { |
||
1939 | 1 | if (!is_readable($fname)) { |
|
1940 | throw new GeneralException('load error: failed to find ' . $fname); |
||
1941 | } |
||
1942 | |||
1943 | 1 | $pi = pathinfo($fname); |
|
1944 | |||
1945 | 1 | $oldImport = $this->importDirs; |
|
1946 | |||
1947 | 1 | $this->importDirs[] = $pi['dirname'] . '/'; |
|
1948 | |||
1949 | 1 | $this->addParsedFile($fname); |
|
1950 | |||
1951 | 1 | $out = $this->compile(file_get_contents($fname), $fname); |
|
1952 | |||
1953 | 1 | $this->importDirs = $oldImport; |
|
1954 | |||
1955 | 1 | if ($outFname !== null) { |
|
1956 | return file_put_contents($outFname, $out); |
||
1957 | } |
||
1958 | |||
1959 | 1 | return $out; |
|
1960 | } |
||
1961 | |||
1962 | /** |
||
1963 | * Based on explicit input/output files does a full change check on cache before compiling. |
||
1964 | * |
||
1965 | * @param string $in |
||
1966 | * @param string $out |
||
1967 | * @param boolean $force |
||
1968 | * |
||
1969 | * @return string Compiled CSS results |
||
1970 | * @throws GeneralException |
||
1971 | */ |
||
1972 | public function checkedCachedCompile($in, $out, $force = false) |
||
1973 | { |
||
1974 | 1 | if (!is_file($in) || !is_readable($in)) { |
|
1975 | throw new GeneralException('Invalid or unreadable input file specified.'); |
||
1976 | } |
||
1977 | 1 | if (is_dir($out) || !is_writable(file_exists($out) ? $out : dirname($out))) { |
|
1978 | throw new GeneralException('Invalid or unwritable output file specified.'); |
||
1979 | } |
||
1980 | |||
1981 | 1 | $outMeta = $out . '.meta'; |
|
1982 | 1 | $metadata = null; |
|
1983 | 1 | if (!$force && is_file($outMeta)) { |
|
1984 | $metadata = unserialize(file_get_contents($outMeta)); |
||
1985 | } |
||
1986 | |||
1987 | 1 | $output = $this->cachedCompile($metadata ?: $in); |
|
1988 | |||
1989 | 1 | if (!$metadata || $metadata['updated'] != $output['updated']) { |
|
1990 | 1 | $css = $output['compiled']; |
|
1991 | 1 | unset($output['compiled']); |
|
1992 | 1 | file_put_contents($out, $css); |
|
1993 | 1 | file_put_contents($outMeta, serialize($output)); |
|
1994 | } else { |
||
1995 | $css = file_get_contents($out); |
||
1996 | } |
||
1997 | |||
1998 | 1 | return $css; |
|
1999 | } |
||
2000 | |||
2001 | /** |
||
2002 | * compile only if changed input has changed or output doesn't exist |
||
2003 | * |
||
2004 | * @param string $in |
||
2005 | * @param string $out |
||
2006 | * |
||
2007 | * @return bool |
||
2008 | * @throws \LesserPhp\Exception\GeneralException |
||
2009 | */ |
||
2010 | public function checkedCompile($in, $out) |
||
2011 | { |
||
2012 | if (!is_file($out) || filemtime($in) > filemtime($out)) { |
||
2013 | $this->compileFile($in, $out); |
||
2014 | |||
2015 | return true; |
||
2016 | } |
||
2017 | |||
2018 | return false; |
||
2019 | } |
||
2020 | |||
2021 | /** |
||
2022 | * Execute lessphp on a .less file or a lessphp cache structure |
||
2023 | * |
||
2024 | * The lessphp cache structure contains information about a specific |
||
2025 | * less file having been parsed. It can be used as a hint for future |
||
2026 | * calls to determine whether or not a rebuild is required. |
||
2027 | * |
||
2028 | * The cache structure contains two important keys that may be used |
||
2029 | * externally: |
||
2030 | * |
||
2031 | * compiled: The final compiled CSS |
||
2032 | * updated: The time (in seconds) the CSS was last compiled |
||
2033 | * |
||
2034 | * The cache structure is a plain-ol' PHP associative array and can |
||
2035 | * be serialized and unserialized without a hitch. |
||
2036 | * |
||
2037 | * @param mixed $in Input |
||
2038 | * @param bool $force Force rebuild? |
||
2039 | * |
||
2040 | * @return array lessphp cache structure |
||
2041 | * @throws \LesserPhp\Exception\GeneralException |
||
2042 | */ |
||
2043 | public function cachedCompile($in, $force = false) |
||
2044 | { |
||
2045 | // assume no root |
||
2046 | 1 | $root = null; |
|
2047 | |||
2048 | 1 | if (is_string($in)) { |
|
2049 | 1 | $root = $in; |
|
2050 | } elseif (is_array($in) && isset($in['root'])) { |
||
2051 | if ($force || !isset($in['files'])) { |
||
2052 | // If we are forcing a recompile or if for some reason the |
||
2053 | // structure does not contain any file information we should |
||
2054 | // specify the root to trigger a rebuild. |
||
2055 | $root = $in['root']; |
||
2056 | } elseif (isset($in['files']) && is_array($in['files'])) { |
||
2057 | foreach ($in['files'] as $fname => $ftime) { |
||
2058 | if (!file_exists($fname) || filemtime($fname) > $ftime) { |
||
2059 | // One of the files we knew about previously has changed |
||
2060 | // so we should look at our incoming root again. |
||
2061 | $root = $in['root']; |
||
2062 | break; |
||
2063 | } |
||
2064 | } |
||
2065 | } |
||
2066 | } else { |
||
2067 | // TODO: Throw an exception? We got neither a string nor something |
||
2068 | // that looks like a compatible lessphp cache structure. |
||
2069 | return null; |
||
2070 | } |
||
2071 | |||
2072 | 1 | if ($root !== null) { |
|
2073 | // If we have a root value which means we should rebuild. |
||
2074 | 1 | $out = []; |
|
2075 | 1 | $out['root'] = $root; |
|
2076 | 1 | $out['compiled'] = $this->compileFile($root); |
|
2077 | 1 | $out['files'] = $this->allParsedFiles; |
|
2078 | 1 | $out['updated'] = time(); |
|
2079 | |||
2080 | 1 | return $out; |
|
2081 | } else { |
||
2082 | // No changes, pass back the structure |
||
2083 | // we were given initially. |
||
2084 | return $in; |
||
2085 | } |
||
2086 | } |
||
2087 | |||
2088 | /** |
||
2089 | * parse and compile buffer |
||
2090 | * This is deprecated |
||
2091 | * |
||
2092 | * @param null $str |
||
2093 | * @param null $initialVariables |
||
2094 | * |
||
2095 | * @return int|string |
||
2096 | * @throws \LesserPhp\Exception\GeneralException |
||
2097 | * @deprecated |
||
2098 | */ |
||
2099 | public function parse($str = null, $initialVariables = null) |
||
2100 | { |
||
2101 | 37 | if (is_array($str)) { |
|
2102 | $initialVariables = $str; |
||
2103 | $str = null; |
||
2104 | } |
||
2105 | |||
2106 | 37 | $oldVars = $this->registeredVars; |
|
2107 | 37 | if ($initialVariables !== null) { |
|
2108 | 1 | $this->setVariables($initialVariables); |
|
2109 | } |
||
2110 | |||
2111 | 37 | if ($str === null) { |
|
2112 | throw new GeneralException('nothing to parse'); |
||
2113 | } else { |
||
2114 | 37 | $out = $this->compile($str); |
|
2115 | } |
||
2116 | |||
2117 | 37 | $this->registeredVars = $oldVars; |
|
2118 | |||
2119 | 37 | return $out; |
|
2120 | } |
||
2121 | |||
2122 | /** |
||
2123 | * @param string $name |
||
2124 | * |
||
2125 | * @return \LesserPhp\Parser |
||
2126 | */ |
||
2127 | protected function makeParser($name) |
||
2128 | { |
||
2129 | 49 | $parser = new Parser($this, $name); |
|
2130 | 49 | $parser->setWriteComments($this->preserveComments); |
|
2131 | |||
2132 | 49 | return $parser; |
|
2133 | } |
||
2134 | |||
2135 | /** |
||
2136 | * @param string $name |
||
2137 | */ |
||
2138 | public function setFormatter($name) |
||
2139 | { |
||
2140 | 1 | $this->formatterName = $name; |
|
2141 | 1 | } |
|
2142 | |||
2143 | /** |
||
2144 | * @return \LesserPhp\Formatter\FormatterInterface |
||
2145 | */ |
||
2146 | protected function newFormatter() |
||
2147 | { |
||
2148 | 49 | $className = 'Lessjs'; |
|
2149 | 49 | if (!empty($this->formatterName)) { |
|
2150 | 1 | if (!is_string($this->formatterName)) { |
|
2151 | return $this->formatterName; |
||
2152 | } |
||
2153 | 1 | $className = $this->formatterName; |
|
2154 | } |
||
2155 | |||
2156 | 49 | $className = '\LesserPhp\Formatter\\' . $className; |
|
2157 | |||
2158 | 49 | return new $className; |
|
2159 | } |
||
2160 | |||
2161 | /** |
||
2162 | * @param bool $preserve |
||
2163 | */ |
||
2164 | public function setPreserveComments($preserve) |
||
2165 | { |
||
2166 | 1 | $this->preserveComments = $preserve; |
|
2167 | 1 | } |
|
2168 | |||
2169 | /** |
||
2170 | * @param string $name |
||
2171 | * @param callable $func |
||
2172 | */ |
||
2173 | public function registerFunction($name, callable $func) |
||
2174 | { |
||
2175 | 1 | $this->libFunctions[$name] = $func; |
|
2176 | 1 | } |
|
2177 | |||
2178 | /** |
||
2179 | * @param string $name |
||
2180 | */ |
||
2181 | public function unregisterFunction($name) |
||
2182 | { |
||
2183 | 1 | unset($this->libFunctions[$name]); |
|
2184 | 1 | } |
|
2185 | |||
2186 | /** |
||
2187 | * @param array $variables |
||
2188 | */ |
||
2189 | public function setVariables(array $variables) |
||
2190 | { |
||
2191 | 1 | $this->registeredVars = array_merge($this->registeredVars, $variables); |
|
2192 | 1 | } |
|
2193 | |||
2194 | /** |
||
2195 | * @param $name |
||
2196 | */ |
||
2197 | public function unsetVariable($name) |
||
2198 | { |
||
2199 | unset($this->registeredVars[$name]); |
||
2200 | } |
||
2201 | |||
2202 | /** |
||
2203 | * @param string[] $dirs |
||
2204 | */ |
||
2205 | public function setImportDirs(array $dirs) |
||
2206 | { |
||
2207 | 38 | $this->importDirs = $dirs; |
|
2208 | 38 | } |
|
2209 | |||
2210 | /** |
||
2211 | * @param string $dir |
||
2212 | */ |
||
2213 | public function addImportDir($dir) |
||
2214 | { |
||
2215 | $this->importDirs[] = $dir; |
||
2216 | } |
||
2217 | |||
2218 | /** |
||
2219 | * @return string[] |
||
2220 | */ |
||
2221 | public function getImportDirs() |
||
2222 | { |
||
2223 | 1 | return $this->importDirs; |
|
2224 | } |
||
2225 | |||
2226 | /** |
||
2227 | * @param string $file |
||
2228 | */ |
||
2229 | public function addParsedFile($file) |
||
2230 | { |
||
2231 | 2 | $this->allParsedFiles[realpath($file)] = filemtime($file); |
|
2232 | 2 | } |
|
2233 | |||
2234 | /** |
||
2235 | * Uses the current value of $this->count to show line and line number |
||
2236 | * |
||
2237 | * @param string $msg |
||
2238 | * |
||
2239 | * @throws GeneralException |
||
2240 | */ |
||
2241 | public function throwError($msg = null) |
||
2242 | { |
||
2243 | if ($this->sourceLoc >= 0) { |
||
2244 | $this->sourceParser->throwError($msg, $this->sourceLoc); |
||
2245 | } |
||
2246 | throw new GeneralException($msg); |
||
2247 | } |
||
2248 | |||
2249 | /** |
||
2250 | * compile file $in to file $out if $in is newer than $out |
||
2251 | * returns true when it compiles, false otherwise |
||
2252 | * |
||
2253 | * @param $in |
||
2254 | * @param $out |
||
2255 | * @param \LesserPhp\Compiler|null $less |
||
2256 | * |
||
2257 | * @return bool |
||
2258 | * @throws \LesserPhp\Exception\GeneralException |
||
2259 | */ |
||
2260 | public static function ccompile($in, $out, Compiler $less = null) |
||
2261 | { |
||
2262 | if ($less === null) { |
||
2263 | $less = new self; |
||
2264 | } |
||
2265 | |||
2266 | return $less->checkedCompile($in, $out); |
||
2267 | } |
||
2268 | |||
2269 | /** |
||
2270 | * @param $in |
||
2271 | * @param bool $force |
||
2272 | * @param \LesserPhp\Compiler|null $less |
||
2273 | * |
||
2274 | * @return array |
||
2275 | * @throws \LesserPhp\Exception\GeneralException |
||
2276 | */ |
||
2277 | public static function cexecute($in, $force = false, Compiler $less = null) |
||
2278 | { |
||
2279 | if ($less === null) { |
||
2280 | $less = new self; |
||
2281 | } |
||
2282 | |||
2283 | return $less->cachedCompile($in, $force); |
||
2284 | } |
||
2285 | |||
2286 | /** |
||
2287 | * prefix of abstract properties |
||
2288 | * |
||
2289 | * @return string |
||
2290 | */ |
||
2291 | public function getVPrefix() |
||
2292 | { |
||
2293 | 49 | return $this->vPrefix; |
|
2294 | } |
||
2295 | |||
2296 | /** |
||
2297 | * prefix of abstract blocks |
||
2298 | * |
||
2299 | * @return string |
||
2300 | */ |
||
2301 | public function getMPrefix() |
||
2302 | { |
||
2303 | 46 | return $this->mPrefix; |
|
2304 | } |
||
2305 | |||
2306 | /** |
||
2307 | * @return string |
||
2308 | */ |
||
2309 | public function getParentSelector() |
||
2310 | { |
||
2311 | 3 | return $this->parentSelector; |
|
2312 | } |
||
2313 | |||
2314 | /** |
||
2315 | * @param int $numberPresicion |
||
2316 | */ |
||
2317 | protected function setNumberPrecision($numberPresicion = null) |
||
2318 | { |
||
2319 | $this->numberPrecision = $numberPresicion; |
||
2320 | } |
||
2321 | |||
2322 | /** |
||
2323 | * @return \LesserPhp\Library\Coerce |
||
2324 | */ |
||
2325 | protected function getCoerce() |
||
2326 | { |
||
2327 | return $this->coerce; |
||
2328 | } |
||
2329 | |||
2330 | public function setImportDisabled() |
||
2331 | { |
||
2332 | 1 | $this->importDisabled = true; |
|
2333 | 1 | } |
|
2334 | |||
2335 | /** |
||
2336 | * @return bool |
||
2337 | */ |
||
2338 | public function isImportDisabled() |
||
2339 | { |
||
2340 | 3 | return $this->importDisabled; |
|
2341 | } |
||
2342 | } |
||
2343 |
There are different options of fixing this problem.
If you want to be on the safe side, you can add an additional type-check:
If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:
Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.