Passed
Push — master ( 2b821f...14f6f1 )
by Marcus
02:54
created

Compiler::getCoerce()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 1
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
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 \LesserPhp\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)
0 ignored issues
show
Duplication introduced by
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...
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 3
    protected function fileExists($name)
201
    {
202 3
        return is_file($name);
203
    }
204
205 48
    public static function compressList($items, $delim)
206
    {
207 48
        if (!isset($items[1]) && isset($items[0])) {
208 48
            return $items[0];
209
        } else {
210 27
            return ['list', $delim, $items];
211
        }
212
    }
213
214 36
    public static function pregQuote($what)
215
    {
216 36
        return preg_quote($what, '/');
217
    }
218
219 3
    protected function tryImport($importPath, $parentBlock, $out)
220
    {
221 3
        if ($importPath[0] === 'function' && $importPath[1] === 'url') {
222 2
            $importPath = $this->flattenList($importPath[2]);
223
        }
224
225 3
        $str = $this->coerce->coerceString($importPath);
226 3
        if ($str === null) {
227 2
            return false;
228
        }
229
230 3
        $url = $this->compileValue($this->functions->e($str));
231
232
        // don't import if it ends in css
233 3
        if (substr_compare($url, '.css', -4, 4) === 0) {
234
            return false;
235
        }
236
237 3
        $realPath = $this->findImport($url);
238
239 3
        if ($realPath === null) {
240 2
            return false;
241
        }
242
243 3
        if ($this->isImportDisabled()) {
244 1
            return [false, '/* import disabled */'];
245
        }
246
247 2
        if (isset($this->allParsedFiles[realpath($realPath)])) {
248 2
            return [false, null];
249
        }
250
251 2
        $this->addParsedFile($realPath);
252 2
        $parser = $this->makeParser($realPath);
253 2
        $root = $parser->parse(file_get_contents($realPath));
254
255
        // set the parents of all the block props
256 2
        foreach ($root->props as $prop) {
257 2
            if ($prop[0] === 'block') {
258 2
                $prop[1]->parent = $parentBlock;
259
            }
260
        }
261
262
        // copy mixins into scope, set their parents
263
        // bring blocks from import into current block
264
        // TODO: need to mark the source parser	these came from this file
265 2
        foreach ($root->children as $childName => $child) {
266 2
            if (isset($parentBlock->children[$childName])) {
267 2
                $parentBlock->children[$childName] = array_merge(
268 2
                    $parentBlock->children[$childName],
269
                    $child
270
                );
271
            } else {
272 2
                $parentBlock->children[$childName] = $child;
273
            }
274
        }
275
276 2
        $pi = pathinfo($realPath);
277 2
        $dir = $pi["dirname"];
278
279 2
        list($top, $bottom) = $this->sortProps($root->props, true);
280 2
        $this->compileImportedProps($top, $parentBlock, $out, $dir);
281
282 2
        return [true, $bottom, $parser, $dir];
283
    }
284
285 2
    protected function compileImportedProps($props, $block, $out, $importDir)
286
    {
287 2
        $oldSourceParser = $this->sourceParser;
288
289 2
        $oldImport = $this->importDirs;
290
291 2
        array_unshift($this->importDirs, $importDir);
292
293 2
        foreach ($props as $prop) {
294 2
            $this->compileProp($prop, $block, $out);
295
        }
296
297 2
        $this->importDirs = $oldImport;
298 2
        $this->sourceParser = $oldSourceParser;
299 2
    }
300
301
    /**
302
     * Recursively compiles a block.
303
     *
304
     * A block is analogous to a CSS block in most cases. A single LESS document
305
     * is encapsulated in a block when parsed, but it does not have parent tags
306
     * so all of it's children appear on the root level when compiled.
307
     *
308
     * Blocks are made up of props and children.
309
     *
310
     * Props are property instructions, array tuples which describe an action
311
     * to be taken, eg. write a property, set a variable, mixin a block.
312
     *
313
     * The children of a block are just all the blocks that are defined within.
314
     * This is used to look up mixins when performing a mixin.
315
     *
316
     * Compiling the block involves pushing a fresh environment on the stack,
317
     * and iterating through the props, compiling each one.
318
     *
319
     * See lessc::compileProp()
320
     *
321
     * @param $block
322
     */
323 49
    protected function compileBlock($block)
324
    {
325 49
        switch ($block->type) {
326 49
            case "root":
327 49
                $this->compileRoot($block);
328 38
                break;
329 46
            case null:
330 46
                $this->compileCSSBlock($block);
331 35
                break;
332 6
            case "media":
333 3
                $this->compileMedia($block);
334 3
                break;
335 4
            case "directive":
336 4
                $name = "@" . $block->name;
337 4
                if (!empty($block->value)) {
338 2
                    $name .= " " . $this->compileValue($this->reduce($block->value));
339
                }
340
341 4
                $this->compileNestedBlock($block, [$name]);
342 4
                break;
343
            default:
344
                $block->parser->throwError("unknown block type: $block->type\n", $block->count);
345
        }
346 38
    }
347
348 46
    protected function compileCSSBlock($block)
349
    {
350 46
        $env = $this->pushEnv($this->env);
351
352 46
        $selectors = $this->compileSelectors($block->tags);
353 46
        $env->setSelectors($this->multiplySelectors($selectors));
354 46
        $out = $this->makeOutputBlock(null, $env->getSelectors());
355
356 46
        $this->scope->children[] = $out;
357 46
        $this->compileProps($block, $out);
358
359 35
        $block->scope = $env; // mixins carry scope with them!
360 35
        $this->popEnv();
361 35
    }
362
363 3
    protected function compileMedia($media)
364
    {
365 3
        $env = $this->pushEnv($this->env, $media);
366 3
        $parentScope = $this->mediaParent($this->scope);
367
368 3
        $query = $this->compileMediaQuery($this->multiplyMedia($env));
369
370 3
        $this->scope = $this->makeOutputBlock($media->type, [$query]);
371 3
        $parentScope->children[] = $this->scope;
372
373 3
        $this->compileProps($media, $this->scope);
374
375 3
        if (count($this->scope->lines) > 0) {
376 3
            $orphanSelelectors = $this->findClosestSelectors();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $orphanSelelectors is correct as $this->findClosestSelectors() (which targets LesserPhp\Compiler::findClosestSelectors()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
377 3
            if ($orphanSelelectors !== null) {
378 3
                $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
379 3
                $orphan->lines = $this->scope->lines;
380 3
                array_unshift($this->scope->children, $orphan);
381 3
                $this->scope->lines = [];
382
            }
383
        }
384
385 3
        $this->scope = $this->scope->parent;
386 3
        $this->popEnv();
387 3
    }
388
389 3
    protected function mediaParent($scope)
390
    {
391 3
        while (!empty($scope->parent)) {
392 1
            if (!empty($scope->type) && $scope->type !== "media") {
393 1
                break;
394
            }
395 1
            $scope = $scope->parent;
396
        }
397
398 3
        return $scope;
399
    }
400
401 4
    protected function compileNestedBlock($block, $selectors)
402
    {
403 4
        $this->pushEnv($this->env, $block);
404 4
        $this->scope = $this->makeOutputBlock($block->type, $selectors);
405 4
        $this->scope->parent->children[] = $this->scope;
406
407 4
        $this->compileProps($block, $this->scope);
408
409 4
        $this->scope = $this->scope->parent;
410 4
        $this->popEnv();
411 4
    }
412
413 49
    protected function compileRoot($root)
414
    {
415 49
        $this->pushEnv($this->env);
416 49
        $this->scope = $this->makeOutputBlock($root->type);
417 49
        $this->compileProps($root, $this->scope);
418 38
        $this->popEnv();
419 38
    }
420
421 49
    protected function compileProps($block, $out)
422
    {
423 49
        foreach ($this->sortProps($block->props) as $prop) {
424 49
            $this->compileProp($prop, $block, $out);
425
        }
426 38
        $out->lines = $this->deduplicate($out->lines);
427 38
    }
428
429
    /**
430
     * Deduplicate lines in a block. Comments are not deduplicated. If a
431
     * duplicate rule is detected, the comments immediately preceding each
432
     * occurence are consolidated.
433
     *
434
     * @param array $lines
435
     *
436
     * @return array
437
     */
438 38
    protected function deduplicate(array $lines)
439
    {
440 38
        $unique = [];
441 38
        $comments = [];
442
443 38
        foreach ($lines as $line) {
444 38
            if (strpos($line, '/*') === 0) {
445 2
                $comments[] = $line;
446 2
                continue;
447
            }
448 37
            if (!in_array($line, $unique)) {
449 37
                $unique[] = $line;
450
            }
451 37
            array_splice($unique, array_search($line, $unique), 0, $comments);
452 37
            $comments = [];
453
        }
454
455 38
        return array_merge($unique, $comments);
456
    }
457
458 49
    protected function sortProps(array $props, $split = false)
459
    {
460 49
        $vars = [];
461 49
        $imports = [];
462 49
        $other = [];
463 49
        $stack = [];
464
465 49
        foreach ($props as $prop) {
466 49
            switch ($prop[0]) {
467 49
                case "comment":
468 1
                    $stack[] = $prop;
469 1
                    break;
470 49
                case "assign":
471 43
                    $stack[] = $prop;
472 43
                    if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
473 23
                        $vars = array_merge($vars, $stack);
474
                    } else {
475 43
                        $other = array_merge($other, $stack);
476
                    }
477 43
                    $stack = [];
478 43
                    break;
479 47
                case "import":
480 3
                    $id = self::$nextImportId++;
481 3
                    $prop[] = $id;
482 3
                    $stack[] = $prop;
483 3
                    $imports = array_merge($imports, $stack);
484 3
                    $other[] = ["import_mixin", $id];
485 3
                    $stack = [];
486 3
                    break;
487
                default:
488 46
                    $stack[] = $prop;
489 46
                    $other = array_merge($other, $stack);
490 46
                    $stack = [];
491 49
                    break;
492
            }
493
        }
494 49
        $other = array_merge($other, $stack);
495
496 49
        if ($split) {
497 2
            return [array_merge($imports, $vars), $other];
498
        } else {
499 49
            return array_merge($imports, $vars, $other);
500
        }
501
    }
502
503 3
    protected function compileMediaQuery(array $queries)
504
    {
505 3
        $compiledQueries = [];
506 3
        foreach ($queries as $query) {
507 3
            $parts = [];
508 3
            foreach ($query as $q) {
509 3
                switch ($q[0]) {
510 3
                    case "mediaType":
511 3
                        $parts[] = implode(" ", array_slice($q, 1));
512 3
                        break;
513 1
                    case "mediaExp":
514 1
                        if (isset($q[2])) {
515 1
                            $parts[] = "($q[1]: " .
516 1
                                $this->compileValue($this->reduce($q[2])) . ")";
517
                        } else {
518 1
                            $parts[] = "($q[1])";
519
                        }
520 1
                        break;
521 1
                    case "variable":
522 1
                        $parts[] = $this->compileValue($this->reduce($q));
523 3
                        break;
524
                }
525
            }
526
527 3
            if (count($parts) > 0) {
528 3
                $compiledQueries[] = implode(" and ", $parts);
529
            }
530
        }
531
532 3
        $out = "@media";
533 3
        if (!empty($parts)) {
534
            $out .= " " .
535 3
                implode($this->formatter->getSelectorSeparator(), $compiledQueries);
536
        }
537
538 3
        return $out;
539
    }
540
541 3
    protected function multiplyMedia(NodeEnv $env = null, array $childQueries = null)
542
    {
543 3
        if (is_null($env) ||
544 3
            (!empty($env->getBlock()->type) && $env->getBlock()->type !== 'media')
545
        ) {
546 3
            return $childQueries;
547
        }
548
549
        // plain old block, skip
550 3
        if (empty($env->getBlock()->type)) {
551 3
            return $this->multiplyMedia($env->getParent(), $childQueries);
552
        }
553
554 3
        $out = [];
555 3
        $queries = $env->getBlock()->queries;
556 3
        if ($childQueries === null) {
557 3
            $out = $queries;
558
        } else {
559 1
            foreach ($queries as $parent) {
560 1
                foreach ($childQueries as $child) {
561 1
                    $out[] = array_merge($parent, $child);
562
                }
563
            }
564
        }
565
566 3
        return $this->multiplyMedia($env->getParent(), $out);
567
    }
568
569 46
    protected function expandParentSelectors(&$tag, $replace)
570
    {
571 46
        $parts = explode("$&$", $tag);
572 46
        $count = 0;
573 46
        foreach ($parts as &$part) {
574 46
            $part = str_replace($this->parentSelector, $replace, $part, $c);
575 46
            $count += $c;
576
        }
577 46
        $tag = implode($this->parentSelector, $parts);
578
579 46
        return $count;
580
    }
581
582 46
    protected function findClosestSelectors()
583
    {
584 46
        $env = $this->env;
585 46
        $selectors = null;
586 46
        while ($env !== null) {
587 46
            if ($env->getSelectors() !== null) {
588 13
                $selectors = $env->getSelectors();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $selectors is correct as $env->getSelectors() (which targets LesserPhp\NodeEnv::getSelectors()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
589 13
                break;
590
            }
591 46
            $env = $env->getParent();
592
        }
593
594 46
        return $selectors;
595
    }
596
597
598
    // multiply $selectors against the nearest selectors in env
599 46
    protected function multiplySelectors(array $selectors)
600
    {
601
        // find parent selectors
602
603 46
        $parentSelectors = $this->findClosestSelectors();
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $parentSelectors is correct as $this->findClosestSelectors() (which targets LesserPhp\Compiler::findClosestSelectors()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
604 46
        if ($parentSelectors === null) {
605
            // kill parent reference in top level selector
606 46
            foreach ($selectors as &$s) {
607 46
                $this->expandParentSelectors($s, "");
608
            }
609
610 46
            return $selectors;
611
        }
612
613 13
        $out = [];
614 13
        foreach ($parentSelectors as $parent) {
0 ignored issues
show
Bug introduced by
The expression $parentSelectors of type null is not traversable.
Loading history...
615 13
            foreach ($selectors as $child) {
616 13
                $count = $this->expandParentSelectors($child, $parent);
617
618
                // don't prepend the parent tag if & was used
619 13
                if ($count > 0) {
620 4
                    $out[] = trim($child);
621
                } else {
622 13
                    $out[] = trim($parent . ' ' . $child);
623
                }
624
            }
625
        }
626
627 13
        return $out;
628
    }
629
630
    // reduces selector expressions
631 46
    protected function compileSelectors(array $selectors)
632
    {
633 46
        $out = [];
634
635 46
        foreach ($selectors as $s) {
636 46
            if (is_array($s)) {
637 4
                list(, $value) = $s;
638 4
                $out[] = trim($this->compileValue($this->reduce($value)));
639
            } else {
640 46
                $out[] = $s;
641
            }
642
        }
643
644 46
        return $out;
645
    }
646
647
    /**
648
     * @param $left
649
     * @param $right
650
     *
651
     * @return bool
652
     */
653 4
    protected function eq($left, $right)
654
    {
655 4
        return $left == $right;
656
    }
657
658 21
    protected function patternMatch($block, $orderedArgs, $keywordArgs)
659
    {
660
        // match the guards if it has them
661
        // any one of the groups must have all its guards pass for a match
662 21
        if (!empty($block->guards)) {
663 5
            $groupPassed = false;
664 5
            foreach ($block->guards as $guardGroup) {
665 5
                foreach ($guardGroup as $guard) {
666 5
                    $this->pushEnv($this->env);
667 5
                    $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
668
669 5
                    $negate = false;
670 5
                    if ($guard[0] === "negate") {
671 1
                        $guard = $guard[1];
672 1
                        $negate = true;
673
                    }
674
675 5
                    $passed = $this->reduce($guard) == self::$TRUE;
676 5
                    if ($negate) {
677 1
                        $passed = !$passed;
678
                    }
679
680 5
                    $this->popEnv();
681
682 5
                    if ($passed) {
683 3
                        $groupPassed = true;
684
                    } else {
685 5
                        $groupPassed = false;
686 5
                        break;
687
                    }
688
                }
689
690 5
                if ($groupPassed) {
691 5
                    break;
692
                }
693
            }
694
695 5
            if (!$groupPassed) {
696 5
                return false;
697
            }
698
        }
699
700 19
        if (empty($block->args)) {
701 12
            return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
702
        }
703
704 14
        $remainingArgs = $block->args;
705 14
        if ($keywordArgs) {
706 2
            $remainingArgs = [];
707 2
            foreach ($block->args as $arg) {
708 2
                if ($arg[0] === "arg" && isset($keywordArgs[$arg[1]])) {
709 2
                    continue;
710
                }
711
712 2
                $remainingArgs[] = $arg;
713
            }
714
        }
715
716 14
        $i = -1; // no args
717
        // try to match by arity or by argument literal
718 14
        foreach ($remainingArgs as $i => $arg) {
719 14
            switch ($arg[0]) {
720 14
                case "lit":
721 3
                    if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) {
722 2
                        return false;
723
                    }
724 3
                    break;
725 14
                case "arg":
726
                    // no arg and no default value
727 14
                    if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
728 3
                        return false;
729
                    }
730 14
                    break;
731 2
                case "rest":
732 2
                    $i--; // rest can be empty
733 14
                    break 2;
734
            }
735
        }
736
737 13
        if ($block->isVararg) {
738 2
            return true; // not having enough is handled above
739
        } else {
740 13
            $numMatched = $i + 1;
741
742
            // greater than because default values always match
743 13
            return $numMatched >= count($orderedArgs);
744
        }
745
    }
746
747 21
    protected function patternMatchAll(array $blocks, $orderedArgs, $keywordArgs, array $skip = [])
748
    {
749 21
        $matches = null;
750 21
        foreach ($blocks as $block) {
751
            // skip seen blocks that don't have arguments
752 21
            if (isset($skip[$block->id]) && !isset($block->args)) {
753 1
                continue;
754
            }
755
756 21
            if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
757 21
                $matches[] = $block;
758
            }
759
        }
760
761 21
        return $matches;
762
    }
763
764
    // attempt to find blocks matched by path and args
765 22
    protected function findBlocks($searchIn, array $path, $orderedArgs, $keywordArgs, array $seen = [])
766
    {
767 22
        if ($searchIn === null) {
768 5
            return null;
769
        }
770 22
        if (isset($seen[$searchIn->id])) {
771 1
            return null;
772
        }
773 22
        $seen[$searchIn->id] = true;
774
775 22
        $name = $path[0];
776
777 22
        if (isset($searchIn->children[$name])) {
778 21
            $blocks = $searchIn->children[$name];
779 21
            if (count($path) === 1) {
780 21
                $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $matches is correct as $this->patternMatchAll($...s, $keywordArgs, $seen) (which targets LesserPhp\Compiler::patternMatchAll()) seems to always return null.

This check looks for function or method calls that always return null and whose return value is assigned to a variable.

class A
{
    function getObject()
    {
        return null;
    }

}

$a = new A();
$object = $a->getObject();

The method getObject() can return nothing but null, so it makes no sense to assign that value to a variable.

The reason is most likely that a function or method is imcomplete or has been reduced for debug purposes.

Loading history...
781 21
                if (!empty($matches)) {
782
                    // This will return all blocks that match in the closest
783
                    // scope that has any matching block, like lessjs
784 21
                    return $matches;
785
                }
786
            } else {
787 3
                $matches = [];
788 3
                foreach ($blocks as $subBlock) {
789 3
                    $subMatches = $this->findBlocks(
790
                        $subBlock,
791 3
                        array_slice($path, 1),
792
                        $orderedArgs,
793
                        $keywordArgs,
794
                        $seen
795
                    );
796
797 3
                    if ($subMatches !== null) {
798 3
                        foreach ($subMatches as $sm) {
799 3
                            $matches[] = $sm;
800
                        }
801
                    }
802
                }
803
804 3
                return count($matches) > 0 ? $matches : null;
805
            }
806
        }
807 22
        if ($searchIn->parent === $searchIn) {
808
            return null;
809
        }
810
811 22
        return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
812
    }
813
814
    // sets all argument names in $args to either the default value
815
    // or the one passed in through $values
816 16
    protected function zipSetArgs(array $args, $orderedValues, $keywordValues)
817
    {
818 16
        $assignedValues = [];
819
820 16
        $i = 0;
821 16
        foreach ($args as $a) {
822 14
            if ($a[0] === "arg") {
823 14
                if (isset($keywordValues[$a[1]])) {
824
                    // has keyword arg
825 2
                    $value = $keywordValues[$a[1]];
826 14
                } elseif (isset($orderedValues[$i])) {
827
                    // has ordered arg
828 13
                    $value = $orderedValues[$i];
829 13
                    $i++;
830 6
                } elseif (isset($a[2])) {
831
                    // has default value
832 6
                    $value = $a[2];
833
                } else {
834
                    throw new GeneralException("Failed to assign arg " . $a[1]);
835
                }
836
837 14
                $value = $this->reduce($value);
838 14
                $this->set($a[1], $value);
839 14
                $assignedValues[] = $value;
840
            } else {
841
                // a lit
842 14
                $i++;
843
            }
844
        }
845
846
        // check for a rest
847 16
        $last = end($args);
848 16
        if ($last[0] === "rest") {
849 2
            $rest = array_slice($orderedValues, count($args) - 1);
850 2
            $this->set($last[1], $this->reduce(["list", " ", $rest]));
851
        }
852
853
        // wow is this the only true use of PHP's + operator for arrays?
854 16
        $this->env->setArguments($assignedValues + $orderedValues);
855 16
    }
856
857
    // compile a prop and update $lines or $blocks appropriately
858 49
    protected function compileProp($prop, $block, $out)
859
    {
860
        // set error position context
861 49
        $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
862
863 49
        switch ($prop[0]) {
864 49
            case 'assign':
865 43
                list(, $name, $value) = $prop;
866 43
                if ($name[0] == $this->vPrefix) {
867 23
                    $this->set($name, $value);
868
                } else {
869 43
                    $out->lines[] = $this->formatter->property(
870
                        $name,
871 43
                        $this->compileValue($this->reduce($value))
872
                    );
873
                }
874 37
                break;
875 47
            case 'block':
876 46
                list(, $child) = $prop;
877 46
                $this->compileBlock($child);
878 35
                break;
879 25
            case 'ruleset':
880 25
            case 'mixin':
881 22
                list(, $path, $args, $suffix) = $prop;
882
883 22
                $orderedArgs = [];
884 22
                $keywordArgs = [];
885 22
                foreach ((array)$args as $arg) {
886 15
                    switch ($arg[0]) {
887 15
                        case "arg":
888 4
                            if (!isset($arg[2])) {
889 3
                                $orderedArgs[] = $this->reduce(["variable", $arg[1]]);
890
                            } else {
891 2
                                $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
892
                            }
893 4
                            break;
894
895 15
                        case "lit":
896 15
                            $orderedArgs[] = $this->reduce($arg[1]);
897 15
                            break;
898
                        default:
899 15
                            throw new GeneralException("Unknown arg type: " . $arg[0]);
900
                    }
901
                }
902
903 22
                $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
904
905 22
                if ($mixins === null) {
906 5
                    $block->parser->throwError("{$prop[1][0]} is undefined", $block->count);
907
                }
908
909 17
                if (strpos($prop[1][0], "$") === 0) {
910
                    //Use Ruleset Logic - Only last element
911 8
                    $mixins = [array_pop($mixins)];
912
                }
913
914 17
                foreach ($mixins as $mixin) {
915 17
                    if ($mixin === $block && !$orderedArgs) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $orderedArgs of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
916
                        continue;
917
                    }
918
919 17
                    $haveScope = false;
920 17
                    if (isset($mixin->parent->scope)) {
921 2
                        $haveScope = true;
922 2
                        $mixinParentEnv = $this->pushEnv($this->env);
923 2
                        $mixinParentEnv->storeParent = $mixin->parent->scope;
0 ignored issues
show
Bug introduced by
The property storeParent does not seem to exist. Did you mean parent?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
924
                    }
925
926 17
                    $haveArgs = false;
927 17
                    if (isset($mixin->args)) {
928 14
                        $haveArgs = true;
929 14
                        $this->pushEnv($this->env);
930 14
                        $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
931
                    }
932
933 17
                    $oldParent = $mixin->parent;
934 17
                    if ($mixin != $block) {
935 17
                        $mixin->parent = $block;
936
                    }
937
938 17
                    foreach ($this->sortProps($mixin->props) as $subProp) {
939 17
                        if ($suffix !== null &&
940 17
                            $subProp[0] === "assign" &&
941 17
                            is_string($subProp[1]) &&
942 17
                            $subProp[1]{0} != $this->vPrefix
943
                        ) {
944 1
                            $subProp[2] = [
945 1
                                'list',
946 1
                                ' ',
947 1
                                [$subProp[2], ['keyword', $suffix]],
948
                            ];
949
                        }
950
951 17
                        $this->compileProp($subProp, $mixin, $out);
952
                    }
953
954 17
                    $mixin->parent = $oldParent;
955
956 17
                    if ($haveArgs) {
957 14
                        $this->popEnv();
958
                    }
959 17
                    if ($haveScope) {
960 17
                        $this->popEnv();
961
                    }
962
                }
963
964 17
                break;
965 5
            case 'raw':
966
                $out->lines[] = $prop[1];
967
                break;
968 5
            case "directive":
969 1
                list(, $name, $value) = $prop;
970 1
                $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)) . ';';
971 1
                break;
972 4
            case "comment":
973 1
                $out->lines[] = $prop[1];
974 1
                break;
975 3
            case "import":
976 3
                list(, $importPath, $importId) = $prop;
977 3
                $importPath = $this->reduce($importPath);
978
979 3
                $result = $this->tryImport($importPath, $block, $out);
980
981 3
                $this->env->addImports($importId, $result === false ?
982 2
                    [false, "@import " . $this->compileValue($importPath) . ";"] :
983 3
                    $result);
984
985 3
                break;
986 3
            case "import_mixin":
987 3
                list(, $importId) = $prop;
988 3
                $import = $this->env->getImports($importId);
989 3
                if ($import[0] === false) {
990 3
                    if (isset($import[1])) {
991 3
                        $out->lines[] = $import[1];
992
                    }
993
                } else {
994 2
                    list(, $bottom, $parser, $importDir) = $import;
0 ignored issues
show
Unused Code introduced by
The assignment to $parser is unused. Consider omitting it like so list($first,,$third).

This checks looks for assignemnts to variables using the list(...) function, where not all assigned variables are subsequently used.

Consider the following code example.

<?php

function returnThreeValues() {
    return array('a', 'b', 'c');
}

list($a, $b, $c) = returnThreeValues();

print $a . " - " . $c;

Only the variables $a and $c are used. There was no need to assign $b.

Instead, the list call could have been.

list($a,, $c) = returnThreeValues();
Loading history...
995 2
                    $this->compileImportedProps($bottom, $block, $out, $importDir);
996
                }
997
998 3
                break;
999
            default:
1000
                $block->parser->throwError("unknown op: {$prop[0]}\n", $block->count);
1001
        }
1002 38
    }
1003
1004
1005
    /**
1006
     * Compiles a primitive value into a CSS property value.
1007
     *
1008
     * Values in lessphp are typed by being wrapped in arrays, their format is
1009
     * typically:
1010
     *
1011
     *     array(type, contents [, additional_contents]*)
1012
     *
1013
     * The input is expected to be reduced. This function will not work on
1014
     * things like expressions and variables.
1015
     *
1016
     * @param array $value
1017
     *
1018
     * @return string
1019
     * @throws \LesserPhp\Exception\GeneralException
1020
     */
1021 38
    public function compileValue(array $value)
1022
    {
1023 38
        switch ($value[0]) {
1024 38
            case 'list':
1025
                // [1] - delimiter
0 ignored issues
show
Unused Code Comprehensibility introduced by
38% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
1026
                // [2] - array of values
1027 21
                return implode($value[1], array_map([$this, 'compileValue'], $value[2]));
1028 38
            case 'raw_color':
1029 8
                if ($this->formatter->getCompressColors()) {
1030
                    return $this->compileValue($this->coerce->coerceColor($value));
1031
                }
1032
1033 8
                return $value[1];
1034 38
            case 'keyword':
1035
                // [1] - the keyword
1036 32
                return $value[1];
1037 36
            case 'number':
1038 32
                list(, $num, $unit) = $value;
1039
                // [1] - the number
1040
                // [2] - the unit
1041 32
                if ($this->numberPrecision !== null) {
1042
                    $num = round($num, $this->numberPrecision);
1043
                }
1044
1045 32
                return $num . $unit;
1046 29
            case 'string':
1047
                // [1] - contents of string (includes quotes)
1048 24
                list(, $delim, $content) = $value;
1049 24
                foreach ($content as &$part) {
1050 24
                    if (is_array($part)) {
1051 24
                        $part = $this->compileValue($part);
1052
                    }
1053
                }
1054
1055 24
                return $delim . implode($content) . $delim;
1056 17
            case 'color':
1057
                // [1] - red component (either number or a %)
1058
                // [2] - green component
1059
                // [3] - blue component
1060
                // [4] - optional alpha component
1061 8
                list(, $r, $g, $b) = $value;
1062 8
                $r = round($r);
1063 8
                $g = round($g);
1064 8
                $b = round($b);
1065
1066 8
                if (count($value) === 5 && $value[4] != 1) { // rgba
1067 3
                    return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $value[4] . ')';
1068
                }
1069
1070 8
                $h = sprintf("#%02x%02x%02x", $r, $g, $b);
1071
1072 8
                if ($this->formatter->getCompressColors()) {
1073
                    // Converting hex color to short notation (e.g. #003399 to #039)
1074 1
                    if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
1075 1
                        $h = '#' . $h[1] . $h[3] . $h[5];
1076
                    }
1077
                }
1078
1079 8
                return $h;
1080
1081 10
            case 'function':
1082 10
                list(, $name, $args) = $value;
1083
1084 10
                return $name . '(' . $this->compileValue($args) . ')';
1085
            default: // assumed to be unit
1086
                throw new GeneralException('unknown value type: ' . $value[0]);
1087
        }
1088
    }
1089
1090
    /**
1091
     * Helper function to get arguments for color manipulation functions.
1092
     * takes a list that contains a color like thing and a percentage
1093
     *
1094
     * @param array $args
1095
     *
1096
     * @return array
1097
     */
1098 2
    public function colorArgs(array $args)
1099
    {
1100 2
        if ($args[0] !== 'list' || count($args[2]) < 2) {
1101 1
            return [['color', 0, 0, 0], 0];
1102
        }
1103 2
        list($color, $delta) = $args[2];
1104 2
        $color = $this->assertions->assertColor($color);
1105 2
        $delta = (float)$delta[1];
1106
1107 2
        return [$color, $delta];
1108
    }
1109
1110
    /**
1111
     * Convert the rgb, rgba, hsl color literals of function type
1112
     * as returned by the parser into values of color type.
1113
     *
1114
     * @param array $func
1115
     *
1116
     * @return bool|mixed
1117
     */
1118 24
    protected function funcToColor(array $func)
1119
    {
1120 24
        $fname = $func[1];
1121 24
        if ($func[2][0] !== 'list') {
1122 6
            return false;
1123
        } // need a list of arguments
1124
        /** @var array $rawComponents */
1125 24
        $rawComponents = $func[2][2];
1126
1127 24
        if ($fname === 'hsl' || $fname === 'hsla') {
1128 1
            $hsl = ['hsl'];
1129 1
            $i = 0;
1130 1
            foreach ($rawComponents as $c) {
1131 1
                $val = $this->reduce($c);
1132 1
                $val = isset($val[1]) ? (float)$val[1] : 0;
1133
1134 1
                if ($i === 0) {
1135 1
                    $clamp = 360;
1136 1
                } elseif ($i < 3) {
1137 1
                    $clamp = 100;
1138
                } else {
1139 1
                    $clamp = 1;
1140
                }
1141
1142 1
                $hsl[] = $this->converter->clamp($val, $clamp);
1143 1
                $i++;
1144
            }
1145
1146 1
            while (count($hsl) < 4) {
1147
                $hsl[] = 0;
1148
            }
1149
1150 1
            return $this->converter->toRGB($hsl);
1151
1152 24
        } elseif ($fname === 'rgb' || $fname === 'rgba') {
1153 4
            $components = [];
1154 4
            $i = 1;
1155 4
            foreach ($rawComponents as $c) {
1156 4
                $c = $this->reduce($c);
1157 4
                if ($i < 4) {
1158 4 View Code Duplication
                    if ($c[0] === "number" && $c[2] === "%") {
0 ignored issues
show
Duplication introduced by
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...
1159 1
                        $components[] = 255 * ($c[1] / 100);
1160
                    } else {
1161 4
                        $components[] = (float)$c[1];
1162
                    }
1163 4 View Code Duplication
                } elseif ($i === 4) {
0 ignored issues
show
Duplication introduced by
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...
1164 4
                    if ($c[0] === "number" && $c[2] === "%") {
1165
                        $components[] = 1.0 * ($c[1] / 100);
1166
                    } else {
1167 4
                        $components[] = (float)$c[1];
1168
                    }
1169
                } else {
1170
                    break;
1171
                }
1172
1173 4
                $i++;
1174
            }
1175 4
            while (count($components) < 3) {
1176
                $components[] = 0;
1177
            }
1178 4
            array_unshift($components, 'color');
1179
1180 4
            return $this->fixColor($components);
1181
        }
1182
1183 23
        return false;
1184
    }
1185
1186
    /**
1187
     * @param array $value
1188
     * @param bool  $forExpression
1189
     *
1190
     * @return array|bool|mixed|null // <!-- dafuq?
1191
     */
1192 48
    public function reduce(array $value, $forExpression = false)
1193
    {
1194 48
        switch ($value[0]) {
1195 48
            case "interpolate":
1196 7
                $reduced = $this->reduce($value[1]);
1197 7
                $var = $this->compileValue($reduced);
1198 7
                $res = $this->reduce(["variable", $this->vPrefix . $var]);
1199
1200 7
                if ($res[0] === "raw_color") {
1201 1
                    $res = $this->coerce->coerceColor($res);
1202
                }
1203
1204 7
                if (empty($value[2])) {
1205 6
                    $res = $this->functions->e($res);
1206
                }
1207
1208 7
                return $res;
1209 48
            case "variable":
1210 28
                $key = $value[1];
1211 28
                if (is_array($key)) {
1212 1
                    $key = $this->reduce($key);
1213 1
                    $key = $this->vPrefix . $this->compileValue($this->functions->e($key));
1214
                }
1215
1216 28
                $seen =& $this->env->seenNames;
1217
1218 28
                if (!empty($seen[$key])) {
1219
                    $this->throwError("infinite loop detected: $key");
1220
                }
1221
1222 28
                $seen[$key] = true;
1223 28
                $out = $this->reduce($this->get($key));
1224 27
                $seen[$key] = false;
1225
1226 27
                return $out;
1227 47
            case "list":
1228 29
                foreach ($value[2] as &$item) {
1229 26
                    $item = $this->reduce($item, $forExpression);
1230
                }
1231
1232 29
                return $value;
1233 47
            case "expression":
1234 19
                return $this->evaluate($value);
1235 47
            case "string":
1236 26
                foreach ($value[2] as &$part) {
1237 26
                    if (is_array($part)) {
1238 11
                        $strip = $part[0] === "variable";
1239 11
                        $part = $this->reduce($part);
1240 11
                        if ($strip) {
1241 26
                            $part = $this->functions->e($part);
1242
                        }
1243
                    }
1244
                }
1245
1246 26
                return $value;
1247 45
            case "escape":
1248 4
                list(, $inner) = $value;
1249
1250 4
                return $this->functions->e($this->reduce($inner));
1251 45
            case "function":
1252 24
                $color = $this->funcToColor($value);
1253 24
                if ($color) {
1254 4
                    return $color;
1255
                }
1256
1257 23
                list(, $name, $args) = $value;
1258 23
                if ($name === "%") {
1259 1
                    $name = "_sprintf";
1260
                }
1261
1262
                // user functions
1263 23
                $f = null;
1264 23
                if (isset($this->libFunctions[$name]) && is_callable($this->libFunctions[$name])) {
1265 1
                    $f = $this->libFunctions[$name];
1266
                }
1267
1268 23
                $func = str_replace('-', '_', $name);
1269
1270 23
                if ($f !== null || method_exists($this->functions, $func)) {
1271 14
                    if ($args[0] === 'list') {
1272 14
                        $args = self::compressList($args[2], $args[1]);
1273
                    }
1274
1275 14
                    if ($f !== null) {
1276 1
                        $ret = $f($this->reduce($args, true), $this);
1277
                    } else {
1278 13
                        $ret = $this->functions->$func($this->reduce($args, true), $this);
1279
                    }
1280 9
                    if ($ret === null) {
1281
                        return [
1282 2
                            "string",
1283 2
                            "",
1284
                            [
1285 2
                                $name,
1286 2
                                "(",
1287 2
                                $args,
1288 2
                                ")",
1289
                            ],
1290
                        ];
1291
                    }
1292
1293
                    // convert to a typed value if the result is a php primitive
1294 8
                    if (is_numeric($ret)) {
1295 3
                        $ret = ['number', $ret, ""];
1296 7
                    } elseif (!is_array($ret)) {
1297 2
                        $ret = ['keyword', $ret];
1298
                    }
1299
1300 8
                    return $ret;
1301
                }
1302
1303
                // plain function, reduce args
1304 10
                $value[2] = $this->reduce($value[2]);
1305
1306 10
                return $value;
1307 40
            case "unary":
1308 6
                list(, $op, $exp) = $value;
1309 6
                $exp = $this->reduce($exp);
1310
1311 6
                if ($exp[0] === "number") {
1312
                    switch ($op) {
1313 6
                        case "+":
1314
                            return $exp;
1315 6
                        case "-":
1316 6
                            $exp[1] *= -1;
1317
1318 6
                            return $exp;
1319
                    }
1320
                }
1321
1322
                return ["string", "", [$op, $exp]];
1323
        }
1324
1325 40
        if ($forExpression) {
1326 22
            switch ($value[0]) {
1327 22
                case "keyword":
1328 5
                    $color = $this->coerce->coerceColor($value);
1329 5
                    if ($color !== null) {
1330 2
                        return $color;
1331
                    }
1332 5
                    break;
1333 22
                case "raw_color":
1334 6
                    return $this->coerce->coerceColor($value);
1335
            }
1336
        }
1337
1338 40
        return $value;
1339
    }
1340
1341
1342
    // turn list of length 1 into value type
1343 2
    protected function flattenList($value)
1344
    {
1345 2
        if ($value[0] === "list" && count($value[2]) === 1) {
1346 2
            return $this->flattenList($value[2][0]);
1347
        }
1348
1349 2
        return $value;
1350
    }
1351
1352 5
    public function toBool($a)
1353
    {
1354 5
        if ($a) {
1355 4
            return self::$TRUE;
1356
        } else {
1357 5
            return self::$FALSE;
1358
        }
1359
    }
1360
1361
    // evaluate an expression
1362 19
    protected function evaluate($exp)
1363
    {
1364 19
        list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
1365
1366 19
        $left = $this->reduce($left, true);
1367 19
        $right = $this->reduce($right, true);
1368
1369 19
        $leftColor = $this->coerce->coerceColor($left);
1370 19
        if ($leftColor !== null) {
1371 5
            $left = $leftColor;
1372
        }
1373
1374 19
        $rightColor = $this->coerce->coerceColor($right);
1375 19
        if ($rightColor !== null) {
1376 5
            $right = $rightColor;
1377
        }
1378
1379 19
        $ltype = $left[0];
1380 19
        $rtype = $right[0];
1381
1382
        // operators that work on all types
1383 19
        if ($op === "and") {
1384
            return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
1385
        }
1386
1387 19
        if ($op === "=") {
1388 1
            return $this->toBool($this->eq($left, $right));
1389
        }
1390
1391 19
        $str = $this->stringConcatenate($left, $right);
1392 19
        if ($op === "+" && $str !== null) {
1393 2
            return $str;
1394
        }
1395
1396
        // type based operators
1397 17
        $fname = "op_${ltype}_${rtype}";
1398 17
        if (is_callable([$this, $fname])) {
1399 17
            $out = $this->$fname($op, $left, $right);
1400 17
            if ($out !== null) {
1401 17
                return $out;
1402
            }
1403
        }
1404
1405
        // make the expression look it did before being parsed
1406 1
        $paddedOp = $op;
1407 1
        if ($whiteBefore) {
1408 1
            $paddedOp = " " . $paddedOp;
1409
        }
1410 1
        if ($whiteAfter) {
1411 1
            $paddedOp .= " ";
1412
        }
1413
1414 1
        return ["string", "", [$left, $paddedOp, $right]];
1415
    }
1416
1417
    protected function stringConcatenate($left, $right)
1418
    {
1419 19
        $strLeft = $this->coerce->coerceString($left);
1420 19
        if ($strLeft !== null) {
1421 2
            if ($right[0] === "string") {
1422 2
                $right[1] = "";
1423
            }
1424 2
            $strLeft[2][] = $right;
1425
1426 2
            return $strLeft;
1427
        }
1428
1429 18
        $strRight = $this->coerce->coerceString($right);
1430 18
        if ($strRight !== null) {
1431 1
            array_unshift($strRight[2], $left);
1432
1433 1
            return $strRight;
1434
        }
1435
1436 17
        return null;
1437
    }
1438
1439
1440
    // make sure a color's components don't go out of bounds
1441
    public function fixColor($c)
1442
    {
1443 7
        foreach (range(1, 3) as $i) {
1444 7
            if ($c[$i] < 0) {
1445
                $c[$i] = 0;
1446
            }
1447 7
            if ($c[$i] > 255) {
1448 7
                $c[$i] = 255;
1449
            }
1450
        }
1451
1452 7
        return $c;
1453
    }
1454
1455
    protected function op_number_color($op, $lft, $rgt)
1456
    {
1457 1
        if ($op === '+' || $op === '*') {
1458 1
            return $this->op_color_number($op, $rgt, $lft);
1459
        }
1460
1461 1
        return null;
1462
    }
1463
1464
    protected function op_color_number($op, $lft, $rgt)
1465
    {
1466 2
        if ($rgt[0] === '%') {
1467
            $rgt[1] /= 100;
1468
        }
1469
1470 2
        return $this->op_color_color(
1471
            $op,
1472
            $lft,
1473 2
            array_fill(1, count($lft) - 1, $rgt[1])
1474
        );
1475
    }
1476
1477
    protected function op_color_color($op, $left, $right)
1478
    {
1479 5
        $out = ['color'];
1480 5
        $max = count($left) > count($right) ? count($left) : count($right);
1481 5
        foreach (range(1, $max - 1) as $i) {
1482 5
            $lval = isset($left[$i]) ? $left[$i] : 0;
1483 5
            $rval = isset($right[$i]) ? $right[$i] : 0;
1484
            switch ($op) {
1485 5
                case '+':
1486 5
                    $out[] = $lval + $rval;
1487 5
                    break;
1488 1
                case '-':
1489 1
                    $out[] = $lval - $rval;
1490 1
                    break;
1491 1
                case '*':
1492 1
                    $out[] = $lval * $rval;
1493 1
                    break;
1494 1
                case '%':
1495 1
                    $out[] = $lval % $rval;
1496 1
                    break;
1497 1
                case '/':
1498 1
                    if ($rval == 0) {
1499
                        throw new GeneralException("evaluate error: can't divide by zero");
1500
                    }
1501 1
                    $out[] = $lval / $rval;
1502 1
                    break;
1503
                default:
1504 5
                    throw new GeneralException('evaluate error: color op number failed on op ' . $op);
1505
            }
1506
        }
1507
1508 5
        return $this->fixColor($out);
1509
    }
1510
1511
1512
    // operator on two numbers
1513
    protected function op_number_number($op, $left, $right)
1514
    {
1515 15
        $unit = empty($left[2]) ? $right[2] : $left[2];
1516
1517 15
        $value = 0;
0 ignored issues
show
Unused Code introduced by
$value is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
1518
        switch ($op) {
1519 15
            case '+':
1520 9
                $value = $left[1] + $right[1];
1521 9
                break;
1522 12
            case '*':
1523 8
                $value = $left[1] * $right[1];
1524 8
                break;
1525 10
            case '-':
1526 6
                $value = $left[1] - $right[1];
1527 6
                break;
1528 9
            case '%':
1529 1
                $value = $left[1] % $right[1];
1530 1
                break;
1531 8
            case '/':
1532 4
                if ($right[1] == 0) {
1533
                    throw new GeneralException('parse error: divide by zero');
1534
                }
1535 4
                $value = $left[1] / $right[1];
1536 4
                break;
1537 5
            case '<':
1538 1
                return $this->toBool($left[1] < $right[1]);
1539 5
            case '>':
1540 3
                return $this->toBool($left[1] > $right[1]);
1541 3
            case '>=':
1542 1
                return $this->toBool($left[1] >= $right[1]);
1543 2
            case '=<':
1544 2
                return $this->toBool($left[1] <= $right[1]);
1545
            default:
1546
                throw new GeneralException('parse error: unknown number operator: ' . $op);
1547
        }
1548
1549 13
        return ["number", $value, $unit];
1550
    }
1551
1552
1553
    /* environment functions */
1554
1555
    protected function makeOutputBlock($type, $selectors = null)
1556
    {
1557 49
        $b = new \stdClass();
1558 49
        $b->lines = [];
1559 49
        $b->children = [];
1560 49
        $b->selectors = $selectors;
1561 49
        $b->type = $type;
1562 49
        $b->parent = $this->scope;
1563
1564 49
        return $b;
1565
    }
1566
1567
    // the state of execution
1568
    protected function pushEnv($parent, $block = null)
1569
    {
1570 49
        $e = new \LesserPhp\NodeEnv();
1571 49
        $e->setParent($parent);
1572 49
        $e->setBlock($block);
1573 49
        $e->setStore([]);
1574
1575 49
        $this->env = $e;
1576
1577 49
        return $e;
1578
    }
1579
1580
    // pop something off the stack
1581
    protected function popEnv()
1582
    {
1583 40
        $old = $this->env;
1584 40
        $this->env = $this->env->getParent();
1585
1586 40
        return $old;
1587
    }
1588
1589
    // set something in the current env
1590
    protected function set($name, $value)
1591
    {
1592 27
        $this->env->addStore($name, $value);
1593 27
    }
1594
1595
1596
    // get the highest occurrence entry for a name
1597
    protected function get($name)
1598
    {
1599 28
        $current = $this->env;
1600
1601
        // track scope to evaluate
1602 28
        $scope_secondary = [];
1603
1604 28
        $isArguments = $name === $this->vPrefix . 'arguments';
1605 28
        while ($current) {
1606 28
            if ($isArguments && count($current->getArguments()) > 0) {
1607 3
                return ['list', ' ', $current->getArguments()];
1608
            }
1609
1610 28
            if (isset($current->getStore()[$name])) {
1611 27
                return $current->getStore()[$name];
1612
            }
1613
            // has secondary scope?
1614 19
            if (isset($current->storeParent)) {
1615
                $scope_secondary[] = $current->storeParent;
0 ignored issues
show
Bug introduced by
The property storeParent does not seem to exist. Did you mean parent?

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1616
            }
1617
1618 19
            if ($current->getParent() !== null) {
1619 19
                $current = $current->getParent();
1620
            } else {
1621 1
                $current = null;
1622
            }
1623
        }
1624
1625 1
        while (count($scope_secondary)) {
1626
            // pop one off
1627
            $current = array_shift($scope_secondary);
1628
            while ($current) {
1629
                if ($isArguments && isset($current->arguments)) {
1630
                    return ['list', ' ', $current->arguments];
1631
                }
1632
1633
                if (isset($current->store[$name])) {
1634
                    return $current->store[$name];
1635
                }
1636
1637
                // has secondary scope?
1638
                if (isset($current->storeParent)) {
1639
                    $scope_secondary[] = $current->storeParent;
1640
                }
1641
1642
                if (isset($current->parent)) {
1643
                    $current = $current->parent;
1644
                } else {
1645
                    $current = null;
1646
                }
1647
            }
1648
        }
1649
1650 1
        throw new GeneralException("variable $name is undefined");
1651
    }
1652
1653
    // inject array of unparsed strings into environment as variables
1654
    protected function injectVariables(array $args)
1655
    {
1656 1
        $this->pushEnv($this->env);
1657 1
        $parser = new \LesserPhp\Parser($this, __METHOD__);
1658 1
        foreach ($args as $name => $strValue) {
1659 1
            if ($name{0} !== '@') {
1660 1
                $name = '@' . $name;
1661
            }
1662 1
            $parser->count = 0;
1663 1
            $parser->buffer = (string)$strValue;
1664 1
            if (!$parser->propertyValue($value)) {
1665
                throw new GeneralException("failed to parse passed in variable $name: $strValue");
1666
            }
1667
1668 1
            $this->set($name, $value);
1669
        }
1670 1
    }
1671
1672
    /**
1673
     * @param string $string
1674
     * @param string $name
1675
     *
1676
     * @return string
1677
     */
1678
    public function compile($string, $name = null)
1679
    {
1680 49
        $locale = setlocale(LC_NUMERIC, 0);
1681 49
        setlocale(LC_NUMERIC, 'C');
1682
1683 49
        $this->parser = $this->makeParser($name);
1684 49
        $root = $this->parser->parse($string);
1685
1686 49
        $this->env = null;
1687 49
        $this->scope = null;
1688 49
        $this->allParsedFiles = [];
1689
1690 49
        $this->formatter = $this->newFormatter();
1691
1692 49
        if (!empty($this->registeredVars)) {
1693 1
            $this->injectVariables($this->registeredVars);
1694
        }
1695
1696 49
        $this->sourceParser = $this->parser; // used for error messages
1697 49
        $this->compileBlock($root);
1698
1699 38
        ob_start();
1700 38
        $this->formatter->block($this->scope);
1701 38
        $out = ob_get_clean();
1702 38
        setlocale(LC_NUMERIC, $locale);
1703
1704 38
        return $out;
1705
    }
1706
1707
    public function compileFile($fname, $outFname = null)
1708
    {
1709 1
        if (!is_readable($fname)) {
1710
            throw new GeneralException('load error: failed to find ' . $fname);
1711
        }
1712
1713 1
        $pi = pathinfo($fname);
1714
1715 1
        $oldImport = $this->importDirs;
1716
1717 1
        $this->importDirs[] = $pi['dirname'] . '/';
1718
1719 1
        $this->addParsedFile($fname);
1720
1721 1
        $out = $this->compile(file_get_contents($fname), $fname);
1722
1723 1
        $this->importDirs = $oldImport;
1724
1725 1
        if ($outFname !== null) {
1726
            return file_put_contents($outFname, $out);
1727
        }
1728
1729 1
        return $out;
1730
    }
1731
1732
    /**
1733
     * Based on explicit input/output files does a full change check on cache before compiling.
1734
     *
1735
     * @param string  $in
1736
     * @param string  $out
1737
     * @param boolean $force
1738
     *
1739
     * @return string Compiled CSS results
1740
     * @throws \Exception
1741
     */
1742
    public function checkedCachedCompile($in, $out, $force = false)
1743
    {
1744 1
        if (!is_file($in) || !is_readable($in)) {
1745
            throw new GeneralException('Invalid or unreadable input file specified.');
1746
        }
1747 1
        if (is_dir($out) || !is_writable(file_exists($out) ? $out : dirname($out))) {
1748
            throw new GeneralException('Invalid or unwritable output file specified.');
1749
        }
1750
1751 1
        $outMeta = $out . '.meta';
1752 1
        $metadata = null;
1753 1
        if (!$force && is_file($outMeta)) {
1754
            $metadata = unserialize(file_get_contents($outMeta));
1755
        }
1756
1757 1
        $output = $this->cachedCompile($metadata ?: $in);
1758
1759 1
        if (!$metadata || $metadata['updated'] != $output['updated']) {
1760 1
            $css = $output['compiled'];
1761 1
            unset($output['compiled']);
1762 1
            file_put_contents($out, $css);
1763 1
            file_put_contents($outMeta, serialize($output));
1764
        } else {
1765
            $css = file_get_contents($out);
1766
        }
1767
1768 1
        return $css;
1769
    }
1770
1771
    // compile only if changed input has changed or output doesn't exist
1772
    public function checkedCompile($in, $out)
1773
    {
1774
        if (!is_file($out) || filemtime($in) > filemtime($out)) {
1775
            $this->compileFile($in, $out);
1776
1777
            return true;
1778
        }
1779
1780
        return false;
1781
    }
1782
1783
    /**
1784
     * Execute lessphp on a .less file or a lessphp cache structure
1785
     *
1786
     * The lessphp cache structure contains information about a specific
1787
     * less file having been parsed. It can be used as a hint for future
1788
     * calls to determine whether or not a rebuild is required.
1789
     *
1790
     * The cache structure contains two important keys that may be used
1791
     * externally:
1792
     *
1793
     * compiled: The final compiled CSS
1794
     * updated: The time (in seconds) the CSS was last compiled
1795
     *
1796
     * The cache structure is a plain-ol' PHP associative array and can
1797
     * be serialized and unserialized without a hitch.
1798
     *
1799
     * @param mixed $in    Input
1800
     * @param bool  $force Force rebuild?
1801
     *
1802
     * @return array lessphp cache structure
1803
     */
1804
    public function cachedCompile($in, $force = false)
1805
    {
1806
        // assume no root
1807 1
        $root = null;
1808
1809 1
        if (is_string($in)) {
1810 1
            $root = $in;
1811
        } elseif (is_array($in) && isset($in['root'])) {
1812
            if ($force || !isset($in['files'])) {
1813
                // If we are forcing a recompile or if for some reason the
1814
                // structure does not contain any file information we should
1815
                // specify the root to trigger a rebuild.
1816
                $root = $in['root'];
1817
            } elseif (isset($in['files']) && is_array($in['files'])) {
1818
                foreach ($in['files'] as $fname => $ftime) {
1819
                    if (!file_exists($fname) || filemtime($fname) > $ftime) {
1820
                        // One of the files we knew about previously has changed
1821
                        // so we should look at our incoming root again.
1822
                        $root = $in['root'];
1823
                        break;
1824
                    }
1825
                }
1826
            }
1827
        } else {
1828
            // TODO: Throw an exception? We got neither a string nor something
1829
            // that looks like a compatible lessphp cache structure.
1830
            return null;
1831
        }
1832
1833 1
        if ($root !== null) {
1834
            // If we have a root value which means we should rebuild.
1835 1
            $out = [];
1836 1
            $out['root'] = $root;
1837 1
            $out['compiled'] = $this->compileFile($root);
1838 1
            $out['files'] = $this->allParsedFiles;
1839 1
            $out['updated'] = time();
1840
1841 1
            return $out;
1842
        } else {
1843
            // No changes, pass back the structure
1844
            // we were given initially.
1845
            return $in;
1846
        }
1847
    }
1848
1849
    // parse and compile buffer
1850
    // This is deprecated
1851
    public function parse($str = null, $initialVariables = null)
1852
    {
1853 37
        if (is_array($str)) {
1854
            $initialVariables = $str;
1855
            $str = null;
1856
        }
1857
1858 37
        $oldVars = $this->registeredVars;
1859 37
        if ($initialVariables !== null) {
1860 1
            $this->setVariables($initialVariables);
1861
        }
1862
1863 37
        if ($str === null) {
1864
            if (empty($this->_parseFile)) {
0 ignored issues
show
Bug introduced by
The property _parseFile 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...
1865
                throw new GeneralException("nothing to parse");
1866
            }
1867
1868
            $out = $this->compileFile($this->_parseFile);
1869
        } else {
1870 37
            $out = $this->compile($str);
1871
        }
1872
1873 37
        $this->registeredVars = $oldVars;
1874
1875 37
        return $out;
1876
    }
1877
1878
    protected function makeParser($name)
1879
    {
1880 49
        $parser = new \LesserPhp\Parser($this, $name);
1881 49
        $parser->writeComments = $this->preserveComments;
0 ignored issues
show
Bug introduced by
The property writeComments does not seem to exist in LesserPhp\Parser.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1882
1883 49
        return $parser;
1884
    }
1885
1886
    public function setFormatter($name)
1887
    {
1888 1
        $this->formatterName = $name;
1889 1
    }
1890
1891
    /**
1892
     * @return \LesserPhp\Formatter\FormatterInterface
1893
     */
1894
    protected function newFormatter()
1895
    {
1896 49
        $className = 'Lessjs';
1897 49
        if (!empty($this->formatterName)) {
1898 1
            if (!is_string($this->formatterName)) {
1899
                return $this->formatterName;
1900
            }
1901 1
            $className = $this->formatterName;
1902
        }
1903
1904 49
        $className = '\LesserPhp\Formatter\\' . $className;
1905
1906 49
        return new $className;
1907
    }
1908
1909
    /**
1910
     * @param bool $preserve
1911
     */
1912
    public function setPreserveComments($preserve)
1913
    {
1914 1
        $this->preserveComments = $preserve;
1915 1
    }
1916
1917
    /**
1918
     * @param string $name
1919
     * @param callable $func
1920
     */
1921
    public function registerFunction($name, callable $func)
1922
    {
1923 1
        $this->libFunctions[$name] = $func;
1924 1
    }
1925
1926
    /**
1927
     * @param string $name
1928
     */
1929
    public function unregisterFunction($name)
1930
    {
1931 1
        unset($this->libFunctions[$name]);
1932 1
    }
1933
1934
    /**
1935
     * @param array $variables
1936
     */
1937
    public function setVariables(array $variables)
1938
    {
1939 1
        $this->registeredVars = array_merge($this->registeredVars, $variables);
0 ignored issues
show
Documentation Bug introduced by
It seems like array_merge($this->registeredVars, $variables) of type array is incompatible with the declared type array<integer,string> of property $registeredVars.

Our type inference engine has found an assignment to a property that is incompatible with the declared type of that property.

Either this assignment is in error or the assigned type should be added to the documentation/type hint for that property..

Loading history...
1940 1
    }
1941
1942
    /**
1943
     * @param $name
1944
     */
1945
    public function unsetVariable($name)
1946
    {
1947
        unset($this->registeredVars[$name]);
1948
    }
1949
1950
    /**
1951
     * @param string[] $dirs
1952
     */
1953
    public function setImportDirs(array $dirs)
1954
    {
1955 38
        $this->importDirs = $dirs;
1956 38
    }
1957
1958
    /**
1959
     * @param string $dir
1960
     */
1961
    public function addImportDir($dir)
1962
    {
1963
        $this->importDirs[] = $dir;
1964
    }
1965
1966
    /**
1967
     * @return string[]
1968
     */
1969
    public function getImportDirs()
1970
    {
1971 1
        return $this->importDirs;
1972
    }
1973
1974
    /**
1975
     * @param string $file
1976
     */
1977
    public function addParsedFile($file)
1978
    {
1979 2
        $this->allParsedFiles[realpath($file)] = filemtime($file);
1980 2
    }
1981
1982
    /**
1983
     * Uses the current value of $this->count to show line and line number
1984
     *
1985
     * @param string $msg
1986
     *
1987
     * @throws \Exception
1988
     */
1989
    public function throwError($msg = null)
1990
    {
1991
        if ($this->sourceLoc >= 0) {
1992
            $this->sourceParser->throwError($msg, $this->sourceLoc);
1993
        }
1994
        throw new GeneralException($msg);
1995
    }
1996
1997
    // compile file $in to file $out if $in is newer than $out
1998
    // returns true when it compiles, false otherwise
1999
    public static function ccompile($in, $out, Compiler $less = null)
2000
    {
2001
        if ($less === null) {
2002
            $less = new self;
2003
        }
2004
2005
        return $less->checkedCompile($in, $out);
2006
    }
2007
2008
    public static function cexecute($in, $force = false, Compiler $less = null)
2009
    {
2010
        if ($less === null) {
2011
            $less = new self;
2012
        }
2013
2014
        return $less->cachedCompile($in, $force);
2015
    }
2016
2017
    /**
2018
     * prefix of abstract properties
2019
     *
2020
     * @return string
2021
     */
2022
    public function getVPrefix()
2023
    {
2024 49
        return $this->vPrefix;
2025
    }
2026
2027
    /**
2028
     * prefix of abstract blocks
2029
     *
2030
     * @return string
2031
     */
2032
    public function getMPrefix()
2033
    {
2034 46
        return $this->mPrefix;
2035
    }
2036
2037
    /**
2038
     * @return string
2039
     */
2040
    public function getParentSelector()
2041
    {
2042 3
        return $this->parentSelector;
2043
    }
2044
2045
    /**
2046
     * @param int $numberPresicion
2047
     */
2048
    protected function setNumberPrecision($numberPresicion = null)
2049
    {
2050
        $this->numberPrecision = $numberPresicion;
2051
    }
2052
2053
    /**
2054
     * @return \LesserPhp\Library\Coerce
2055
     */
2056
    protected function getCoerce()
2057
    {
2058
        return $this->coerce;
2059
    }
2060
2061
    public function setImportDisabled()
2062
    {
2063 1
        $this->importDisabled = true;
2064 1
    }
2065
2066
    /**
2067
     * @return bool
2068
     */
2069
    public function isImportDisabled()
2070
    {
2071 3
        return $this->importDisabled;
2072
    }
2073
}
2074