Completed
Push — master ( 6ece6a...9c4912 )
by Marcus
02:53
created

Compiler::patternMatch()   F

Complexity

Conditions 25
Paths 355

Size

Total Lines 88
Code Lines 52

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 50
CRAP Score 25

Importance

Changes 0
Metric Value
dl 0
loc 88
ccs 50
cts 50
cp 1
rs 3.5926
c 0
b 0
f 0
cc 25
eloc 52
nc 355
nop 3
crap 25

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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 64
    public function __construct()
174
    {
175 64
        $this->coerce = new Coerce();
176 64
        $this->assertions = new Assertions($this->coerce);
177 64
        $this->converter = new Converter();
178 64
        $this->functions = new Functions($this->assertions, $this->coerce, $this, $this->converter);
179 64
    }
180
181
    /**
182
     * @param array $items
183
     * @param       $delim
184
     *
185
     * @return array
186
     */
187 48
    public static function compressList(array $items, $delim)
188
    {
189 48
        if (!isset($items[1]) && isset($items[0])) {
190 48
            return $items[0];
191
        } else {
192 27
            return ['list', $delim, $items];
193
        }
194
    }
195
196
    /**
197
     * @param string $what
198
     *
199
     * @return string
200
     */
201 36
    public static function pregQuote($what)
202
    {
203 36
        return preg_quote($what, '/');
204
    }
205
206
    /**
207
     * @param array $importPath
208
     * @param       $parentBlock
209
     * @param       $out
210
     *
211
     * @return array|false
212
     * @throws \LesserPhp\Exception\GeneralException
213
     */
214 3
    protected function tryImport(array $importPath, $parentBlock, $out)
215
    {
216 3
        if ($importPath[0] === 'function' && $importPath[1] === 'url') {
217 2
            $importPath = $this->flattenList($importPath[2]);
218
        }
219
220 3
        $str = $this->coerce->coerceString($importPath);
221 3
        if ($str === null) {
222 2
            return false;
223
        }
224
225 3
        $url = $this->compileValue($this->functions->e($str));
226
227
        // don't import if it ends in css
228 3
        if (substr_compare($url, '.css', -4, 4) === 0) {
229
            return false;
230
        }
231
232 3
        $realPath = $this->functions->findImport($url);
233
234 3
        if ($realPath === null) {
235 2
            return false;
236
        }
237
238 3
        if ($this->isImportDisabled()) {
239 1
            return [false, '/* import disabled */'];
240
        }
241
242 2
        if (isset($this->allParsedFiles[realpath($realPath)])) {
243 2
            return [false, null];
244
        }
245
246 2
        $this->addParsedFile($realPath);
247 2
        $parser = $this->makeParser($realPath);
248 2
        $root = $parser->parse(file_get_contents($realPath));
249
250
        // set the parents of all the block props
251 2
        foreach ($root->props as $prop) {
252 2
            if ($prop[0] === 'block') {
253 2
                $prop[1]->parent = $parentBlock;
254
            }
255
        }
256
257
        // copy mixins into scope, set their parents
258
        // bring blocks from import into current block
259
        // TODO: need to mark the source parser	these came from this file
260 2
        foreach ($root->children as $childName => $child) {
261 2
            if (isset($parentBlock->children[$childName])) {
262 2
                $parentBlock->children[$childName] = array_merge(
263 2
                    $parentBlock->children[$childName],
264
                    $child
265
                );
266
            } else {
267 2
                $parentBlock->children[$childName] = $child;
268
            }
269
        }
270
271 2
        $pi = pathinfo($realPath);
272 2
        $dir = $pi["dirname"];
273
274 2
        list($top, $bottom) = $this->sortProps($root->props, true);
275 2
        $this->compileImportedProps($top, $parentBlock, $out, $dir);
276
277 2
        return [true, $bottom, $parser, $dir];
278
    }
279
280
    /**
281
     * @param array  $props
282
     * @param        $block
283
     * @param        $out
284
     * @param string $importDir
285
     */
286 2
    protected function compileImportedProps(array $props, $block, $out, $importDir)
287
    {
288 2
        $oldSourceParser = $this->sourceParser;
289
290 2
        $oldImport = $this->importDirs;
291
292 2
        array_unshift($this->importDirs, $importDir);
293
294 2
        foreach ($props as $prop) {
295 2
            $this->compileProp($prop, $block, $out);
296
        }
297
298 2
        $this->importDirs = $oldImport;
299 2
        $this->sourceParser = $oldSourceParser;
300 2
    }
301
302
    /**
303
     * Recursively compiles a block.
304
     *
305
     * A block is analogous to a CSS block in most cases. A single LESS document
306
     * is encapsulated in a block when parsed, but it does not have parent tags
307
     * so all of it's children appear on the root level when compiled.
308
     *
309
     * Blocks are made up of props and children.
310
     *
311
     * Props are property instructions, array tuples which describe an action
312
     * to be taken, eg. write a property, set a variable, mixin a block.
313
     *
314
     * The children of a block are just all the blocks that are defined within.
315
     * This is used to look up mixins when performing a mixin.
316
     *
317
     * Compiling the block involves pushing a fresh environment on the stack,
318
     * and iterating through the props, compiling each one.
319
     *
320
     * See lessc::compileProp()
321
     *
322
     * @param $block
323
     *
324
     * @throws \LesserPhp\Exception\GeneralException
325
     */
326 49
    protected function compileBlock($block)
327
    {
328 49
        switch ($block->type) {
329 49
            case "root":
330 49
                $this->compileRoot($block);
331 38
                break;
332 46
            case null:
333 46
                $this->compileCSSBlock($block);
334 35
                break;
335 6
            case "media":
336 3
                $this->compileMedia($block);
337 3
                break;
338 4
            case "directive":
339 4
                $name = "@" . $block->name;
340 4
                if (!empty($block->value)) {
341 2
                    $name .= " " . $this->compileValue($this->reduce($block->value));
342
                }
343
344 4
                $this->compileNestedBlock($block, [$name]);
345 4
                break;
346
            default:
347
                $block->parser->throwError("unknown block type: $block->type\n", $block->count);
348
        }
349 38
    }
350
351
    /**
352
     * @param $block
353
     *
354
     * @throws \LesserPhp\Exception\GeneralException
355
     */
356 46
    protected function compileCSSBlock($block)
357
    {
358 46
        $env = $this->pushEnv($this->env);
359
360 46
        $selectors = $this->compileSelectors($block->tags);
361 46
        $env->setSelectors($this->multiplySelectors($selectors));
362 46
        $out = $this->makeOutputBlock(null, $env->getSelectors());
0 ignored issues
show
Bug introduced by
It seems like $env->getSelectors() targeting LesserPhp\NodeEnv::getSelectors() can also be of type array; however, LesserPhp\Compiler::makeOutputBlock() does only seem to accept null, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
363
364 46
        $this->scope->children[] = $out;
365 46
        $this->compileProps($block, $out);
366
367 35
        $block->scope = $env; // mixins carry scope with them!
368 35
        $this->popEnv();
369 35
    }
370
371
    /**
372
     * @param $media
373
     */
374 3
    protected function compileMedia($media)
375
    {
376 3
        $env = $this->pushEnv($this->env, $media);
377 3
        $parentScope = $this->mediaParent($this->scope);
378
379 3
        $query = $this->compileMediaQuery($this->multiplyMedia($env));
0 ignored issues
show
Bug introduced by
It seems like $this->multiplyMedia($env) targeting LesserPhp\Compiler::multiplyMedia() can also be of type null; however, LesserPhp\Compiler::compileMediaQuery() does only seem to accept array, maybe add an additional type check?

This check looks at variables that are passed out again to other methods.

If the outgoing method call has stricter type requirements than the method itself, an issue is raised.

An additional type check may prevent trouble.

Loading history...
380
381 3
        $this->scope = $this->makeOutputBlock($media->type, [$query]);
382 3
        $parentScope->children[] = $this->scope;
383
384 3
        $this->compileProps($media, $this->scope);
385
386 3
        if (count($this->scope->lines) > 0) {
387 3
            $orphanSelelectors = $this->findClosestSelectors();
388 3
            if ($orphanSelelectors !== null) {
389 3
                $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
390 3
                $orphan->lines = $this->scope->lines;
391 3
                array_unshift($this->scope->children, $orphan);
392 3
                $this->scope->lines = [];
393
            }
394
        }
395
396 3
        $this->scope = $this->scope->parent;
397 3
        $this->popEnv();
398 3
    }
399
400
    /**
401
     * @param $scope
402
     *
403
     * @return mixed
404
     */
405 3
    protected function mediaParent($scope)
406
    {
407 3
        while (!empty($scope->parent)) {
408 1
            if (!empty($scope->type) && $scope->type !== "media") {
409 1
                break;
410
            }
411 1
            $scope = $scope->parent;
412
        }
413
414 3
        return $scope;
415
    }
416
417
    /**
418
     * @param          $block
419
     * @param string[] $selectors
420
     */
421 4
    protected function compileNestedBlock($block, array $selectors)
422
    {
423 4
        $this->pushEnv($this->env, $block);
424 4
        $this->scope = $this->makeOutputBlock($block->type, $selectors);
425 4
        $this->scope->parent->children[] = $this->scope;
426
427 4
        $this->compileProps($block, $this->scope);
428
429 4
        $this->scope = $this->scope->parent;
430 4
        $this->popEnv();
431 4
    }
432
433
    /**
434
     * @param $root
435
     */
436 49
    protected function compileRoot($root)
437
    {
438 49
        $this->pushEnv($this->env);
439 49
        $this->scope = $this->makeOutputBlock($root->type);
440 49
        $this->compileProps($root, $this->scope);
441 38
        $this->popEnv();
442 38
    }
443
444
    /**
445
     * @param $block
446
     * @param $out
447
     *
448
     * @throws \LesserPhp\Exception\GeneralException
449
     */
450 49
    protected function compileProps($block, $out)
451
    {
452 49
        foreach ($this->sortProps($block->props) as $prop) {
453 49
            $this->compileProp($prop, $block, $out);
454
        }
455 38
        $out->lines = $this->deduplicate($out->lines);
456 38
    }
457
458
    /**
459
     * Deduplicate lines in a block. Comments are not deduplicated. If a
460
     * duplicate rule is detected, the comments immediately preceding each
461
     * occurence are consolidated.
462
     *
463
     * @param array $lines
464
     *
465
     * @return array
466
     */
467 38
    protected function deduplicate(array $lines)
468
    {
469 38
        $unique = [];
470 38
        $comments = [];
471
472 38
        foreach ($lines as $line) {
473 38
            if (strpos($line, '/*') === 0) {
474 2
                $comments[] = $line;
475 2
                continue;
476
            }
477 37
            if (!in_array($line, $unique)) {
478 37
                $unique[] = $line;
479
            }
480 37
            array_splice($unique, array_search($line, $unique), 0, $comments);
481 37
            $comments = [];
482
        }
483
484 38
        return array_merge($unique, $comments);
485
    }
486
487
    /**
488
     * @param array $props
489
     * @param bool  $split
490
     *
491
     * @return array
492
     */
493 49
    protected function sortProps(array $props, $split = false)
494
    {
495 49
        $vars = [];
496 49
        $imports = [];
497 49
        $other = [];
498 49
        $stack = [];
499
500 49
        foreach ($props as $prop) {
501 49
            switch ($prop[0]) {
502 49
                case "comment":
503 1
                    $stack[] = $prop;
504 1
                    break;
505 49
                case "assign":
506 43
                    $stack[] = $prop;
507 43
                    if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
508 23
                        $vars = array_merge($vars, $stack);
509
                    } else {
510 43
                        $other = array_merge($other, $stack);
511
                    }
512 43
                    $stack = [];
513 43
                    break;
514 47
                case "import":
515 3
                    $id = self::$nextImportId++;
516 3
                    $prop[] = $id;
517 3
                    $stack[] = $prop;
518 3
                    $imports = array_merge($imports, $stack);
519 3
                    $other[] = ["import_mixin", $id];
520 3
                    $stack = [];
521 3
                    break;
522
                default:
523 46
                    $stack[] = $prop;
524 46
                    $other = array_merge($other, $stack);
525 46
                    $stack = [];
526 49
                    break;
527
            }
528
        }
529 49
        $other = array_merge($other, $stack);
530
531 49
        if ($split) {
532 2
            return [array_merge($imports, $vars), $other];
533
        } else {
534 49
            return array_merge($imports, $vars, $other);
535
        }
536
    }
537
538
    /**
539
     * @param array $queries
540
     *
541
     * @return string
542
     */
543 3
    protected function compileMediaQuery(array $queries)
544
    {
545 3
        $compiledQueries = [];
546 3
        foreach ($queries as $query) {
547 3
            $parts = [];
548 3
            foreach ($query as $q) {
549 3
                switch ($q[0]) {
550 3
                    case "mediaType":
551 3
                        $parts[] = implode(" ", array_slice($q, 1));
552 3
                        break;
553 1
                    case "mediaExp":
554 1
                        if (isset($q[2])) {
555 1
                            $parts[] = "($q[1]: " .
556 1
                                $this->compileValue($this->reduce($q[2])) . ")";
557
                        } else {
558 1
                            $parts[] = "($q[1])";
559
                        }
560 1
                        break;
561 1
                    case "variable":
562 1
                        $parts[] = $this->compileValue($this->reduce($q));
563 3
                        break;
564
                }
565
            }
566
567 3
            if (count($parts) > 0) {
568 3
                $compiledQueries[] = implode(" and ", $parts);
569
            }
570
        }
571
572 3
        $out = "@media";
573 3
        if (!empty($parts)) {
574
            $out .= " " .
575 3
                implode($this->formatter->getSelectorSeparator(), $compiledQueries);
576
        }
577
578 3
        return $out;
579
    }
580
581
    /**
582
     * @param \LesserPhp\NodeEnv $env
583
     * @param array              $childQueries
584
     *
585
     * @return array
586
     */
587 3
    protected function multiplyMedia(NodeEnv $env = null, array $childQueries = null)
588
    {
589 3
        if ($env === null ||
590 3
            (!empty($env->getBlock()->type) && $env->getBlock()->type !== 'media')
591
        ) {
592 3
            return $childQueries;
593
        }
594
595
        // plain old block, skip
596 3
        if (empty($env->getBlock()->type)) {
597 3
            return $this->multiplyMedia($env->getParent(), $childQueries);
598
        }
599
600 3
        $out = [];
601 3
        $queries = $env->getBlock()->queries;
602 3
        if ($childQueries === null) {
603 3
            $out = $queries;
604
        } else {
605 1
            foreach ($queries as $parent) {
606 1
                foreach ($childQueries as $child) {
607 1
                    $out[] = array_merge($parent, $child);
608
                }
609
            }
610
        }
611
612 3
        return $this->multiplyMedia($env->getParent(), $out);
613
    }
614
615
    /**
616
     * @param $tag
617
     * @param $replace
618
     *
619
     * @return int
620
     */
621 46
    protected function expandParentSelectors(&$tag, $replace)
622
    {
623 46
        $parts = explode("$&$", $tag);
624 46
        $count = 0;
625 46
        foreach ($parts as &$part) {
626 46
            $part = str_replace($this->parentSelector, $replace, $part, $c);
627 46
            $count += $c;
628
        }
629 46
        $tag = implode($this->parentSelector, $parts);
630
631 46
        return $count;
632
    }
633
634
    /**
635
     * @return array|null
636
     */
637 46
    protected function findClosestSelectors()
638
    {
639 46
        $env = $this->env;
640 46
        $selectors = null;
641 46
        while ($env !== null) {
642 46
            if ($env->getSelectors() !== null) {
643 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...
644 13
                break;
645
            }
646 46
            $env = $env->getParent();
647
        }
648
649 46
        return $selectors;
650
    }
651
652
653
    /**
654
     *  multiply $selectors against the nearest selectors in env
655
     *
656
     * @param array $selectors
657
     *
658
     * @return array
659
     */
660 46
    protected function multiplySelectors(array $selectors)
661
    {
662
        // find parent selectors
663
664 46
        $parentSelectors = $this->findClosestSelectors();
665 46
        if ($parentSelectors === null) {
666
            // kill parent reference in top level selector
667 46
            foreach ($selectors as &$s) {
668 46
                $this->expandParentSelectors($s, "");
669
            }
670
671 46
            return $selectors;
672
        }
673
674 13
        $out = [];
675 13
        foreach ($parentSelectors as $parent) {
676 13
            foreach ($selectors as $child) {
677 13
                $count = $this->expandParentSelectors($child, $parent);
678
679
                // don't prepend the parent tag if & was used
680 13
                if ($count > 0) {
681 4
                    $out[] = trim($child);
682
                } else {
683 13
                    $out[] = trim($parent . ' ' . $child);
684
                }
685
            }
686
        }
687
688 13
        return $out;
689
    }
690
691
    /**
692
     * reduces selector expressions
693
     *
694
     * @param array $selectors
695
     *
696
     * @return array
697
     * @throws \LesserPhp\Exception\GeneralException
698
     */
699 46
    protected function compileSelectors(array $selectors)
700
    {
701 46
        $out = [];
702
703 46
        foreach ($selectors as $s) {
704 46
            if (is_array($s)) {
705 4
                list(, $value) = $s;
706 4
                $out[] = trim($this->compileValue($this->reduce($value)));
707
            } else {
708 46
                $out[] = $s;
709
            }
710
        }
711
712 46
        return $out;
713
    }
714
715
    /**
716
     * @param $left
717
     * @param $right
718
     *
719
     * @return bool
720
     */
721 4
    protected function equals($left, $right)
722
    {
723 4
        return $left == $right;
724
    }
725
726
    /**
727
     * @param $block
728
     * @param $orderedArgs
729
     * @param $keywordArgs
730
     *
731
     * @return bool
732
     */
733 21
    protected function patternMatch($block, $orderedArgs, $keywordArgs)
734
    {
735
        // match the guards if it has them
736
        // any one of the groups must have all its guards pass for a match
737 21
        if (!empty($block->guards)) {
738 5
            $groupPassed = false;
739 5
            foreach ($block->guards as $guardGroup) {
740 5
                foreach ($guardGroup as $guard) {
741 5
                    $this->pushEnv($this->env);
742 5
                    $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
743
744 5
                    $negate = false;
745 5
                    if ($guard[0] === "negate") {
746 1
                        $guard = $guard[1];
747 1
                        $negate = true;
748
                    }
749
750 5
                    $passed = $this->reduce($guard) == self::$TRUE;
751 5
                    if ($negate) {
752 1
                        $passed = !$passed;
753
                    }
754
755 5
                    $this->popEnv();
756
757 5
                    if ($passed) {
758 3
                        $groupPassed = true;
759
                    } else {
760 5
                        $groupPassed = false;
761 5
                        break;
762
                    }
763
                }
764
765 5
                if ($groupPassed) {
766 5
                    break;
767
                }
768
            }
769
770 5
            if (!$groupPassed) {
771 5
                return false;
772
            }
773
        }
774
775 19
        if (empty($block->args)) {
776 12
            return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
777
        }
778
779 14
        $remainingArgs = $block->args;
780 14
        if ($keywordArgs) {
781 2
            $remainingArgs = [];
782 2
            foreach ($block->args as $arg) {
783 2
                if ($arg[0] === "arg" && isset($keywordArgs[$arg[1]])) {
784 2
                    continue;
785
                }
786
787 2
                $remainingArgs[] = $arg;
788
            }
789
        }
790
791 14
        $i = -1; // no args
792
        // try to match by arity or by argument literal
793 14
        foreach ($remainingArgs as $i => $arg) {
794 14
            switch ($arg[0]) {
795 14
                case "lit":
796 3
                    if (empty($orderedArgs[$i]) || !$this->equals($arg[1], $orderedArgs[$i])) {
797 2
                        return false;
798
                    }
799 3
                    break;
800 14
                case "arg":
801
                    // no arg and no default value
802 14
                    if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
803 3
                        return false;
804
                    }
805 14
                    break;
806 2
                case "rest":
807 2
                    $i--; // rest can be empty
808 14
                    break 2;
809
            }
810
        }
811
812 13
        if ($block->isVararg) {
813 2
            return true; // not having enough is handled above
814
        } else {
815 13
            $numMatched = $i + 1;
816
817
            // greater than because default values always match
818 13
            return $numMatched >= count($orderedArgs);
819
        }
820
    }
821
822
    /**
823
     * @param array $blocks
824
     * @param       $orderedArgs
825
     * @param       $keywordArgs
826
     * @param array $skip
827
     *
828
     * @return array|null
829
     */
830 21
    protected function patternMatchAll(array $blocks, $orderedArgs, $keywordArgs, array $skip = [])
831
    {
832 21
        $matches = null;
833 21
        foreach ($blocks as $block) {
834
            // skip seen blocks that don't have arguments
835 21
            if (isset($skip[$block->id]) && !isset($block->args)) {
836 1
                continue;
837
            }
838
839 21
            if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
840 21
                $matches[] = $block;
841
            }
842
        }
843
844 21
        return $matches;
845
    }
846
847
    /**
848
     * attempt to find blocks matched by path and args
849
     *
850
     * @param       $searchIn
851
     * @param array $path
852
     * @param       $orderedArgs
853
     * @param       $keywordArgs
854
     * @param array $seen
855
     *
856
     * @return array|null
857
     */
858 22
    protected function findBlocks($searchIn, array $path, $orderedArgs, $keywordArgs, array $seen = [])
859
    {
860 22
        if ($searchIn === null) {
861 5
            return null;
862
        }
863 22
        if (isset($seen[$searchIn->id])) {
864 1
            return null;
865
        }
866 22
        $seen[$searchIn->id] = true;
867
868 22
        $name = $path[0];
869
870 22
        if (isset($searchIn->children[$name])) {
871 21
            $blocks = $searchIn->children[$name];
872 21
            if (count($path) === 1) {
873 21
                $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
874 21
                if (!empty($matches)) {
875
                    // This will return all blocks that match in the closest
876
                    // scope that has any matching block, like lessjs
877 21
                    return $matches;
878
                }
879
            } else {
880 3
                $matches = [];
881 3
                foreach ($blocks as $subBlock) {
882 3
                    $subMatches = $this->findBlocks(
883
                        $subBlock,
884 3
                        array_slice($path, 1),
885
                        $orderedArgs,
886
                        $keywordArgs,
887
                        $seen
888
                    );
889
890 3
                    if ($subMatches !== null) {
891 3
                        foreach ($subMatches as $sm) {
892 3
                            $matches[] = $sm;
893
                        }
894
                    }
895
                }
896
897 3
                return count($matches) > 0 ? $matches : null;
898
            }
899
        }
900 22
        if ($searchIn->parent === $searchIn) {
901
            return null;
902
        }
903
904 22
        return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
905
    }
906
907
    /**
908
     * sets all argument names in $args to either the default value
909
     * or the one passed in through $values
910
     *
911
     * @param array $args
912
     * @param       $orderedValues
913
     * @param       $keywordValues
914
     *
915
     * @throws \LesserPhp\Exception\GeneralException
916
     */
917 16
    protected function zipSetArgs(array $args, $orderedValues, $keywordValues)
918
    {
919 16
        $assignedValues = [];
920
921 16
        $i = 0;
922 16
        foreach ($args as $a) {
923 14
            if ($a[0] === "arg") {
924 14
                if (isset($keywordValues[$a[1]])) {
925
                    // has keyword arg
926 2
                    $value = $keywordValues[$a[1]];
927 14
                } elseif (isset($orderedValues[$i])) {
928
                    // has ordered arg
929 13
                    $value = $orderedValues[$i];
930 13
                    $i++;
931 6
                } elseif (isset($a[2])) {
932
                    // has default value
933 6
                    $value = $a[2];
934
                } else {
935
                    throw new GeneralException('Failed to assign arg ' . $a[1]);
936
                }
937
938 14
                $value = $this->reduce($value);
939 14
                $this->set($a[1], $value);
940 14
                $assignedValues[] = $value;
941
            } else {
942
                // a lit
943 14
                $i++;
944
            }
945
        }
946
947
        // check for a rest
948 16
        $last = end($args);
949 16
        if ($last[0] === "rest") {
950 2
            $rest = array_slice($orderedValues, count($args) - 1);
951 2
            $this->set($last[1], $this->reduce(["list", " ", $rest]));
952
        }
953
954
        // wow is this the only true use of PHP's + operator for arrays?
955 16
        $this->env->setArguments($assignedValues + $orderedValues);
956 16
    }
957
958
    /**
959
     * compile a prop and update $lines or $blocks appropriately
960
     *
961
     * @param $prop
962
     * @param $block
963
     * @param $out
964
     *
965
     * @throws \LesserPhp\Exception\GeneralException
966
     */
967 49
    protected function compileProp($prop, $block, $out)
968
    {
969
        // set error position context
970 49
        $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
971
972 49
        switch ($prop[0]) {
973 49
            case 'assign':
974 43
                list(, $name, $value) = $prop;
975 43
                if ($name[0] == $this->vPrefix) {
976 23
                    $this->set($name, $value);
977
                } else {
978 43
                    $out->lines[] = $this->formatter->property(
979
                        $name,
980 43
                        $this->compileValue($this->reduce($value))
981
                    );
982
                }
983 37
                break;
984 47
            case 'block':
985 46
                list(, $child) = $prop;
986 46
                $this->compileBlock($child);
987 35
                break;
988 25
            case 'ruleset':
989 25
            case 'mixin':
990 22
                list(, $path, $args, $suffix) = $prop;
991
992 22
                $orderedArgs = [];
993 22
                $keywordArgs = [];
994 22
                foreach ((array)$args as $arg) {
995 15
                    switch ($arg[0]) {
996 15
                        case "arg":
997 4
                            if (!isset($arg[2])) {
998 3
                                $orderedArgs[] = $this->reduce(["variable", $arg[1]]);
999
                            } else {
1000 2
                                $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
1001
                            }
1002 4
                            break;
1003
1004 15
                        case "lit":
1005 15
                            $orderedArgs[] = $this->reduce($arg[1]);
1006 15
                            break;
1007
                        default:
1008 15
                            throw new GeneralException("Unknown arg type: " . $arg[0]);
1009
                    }
1010
                }
1011
1012 22
                $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
1013
1014 22
                if ($mixins === null) {
1015 5
                    $block->parser->throwError("{$prop[1][0]} is undefined", $block->count);
1016
                }
1017
1018 17
                if (strpos($prop[1][0], "$") === 0) {
1019
                    //Use Ruleset Logic - Only last element
1020 8
                    $mixins = [array_pop($mixins)];
1021
                }
1022
1023 17
                foreach ($mixins as $mixin) {
0 ignored issues
show
Bug introduced by
The expression $mixins of type null|array is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. 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:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
1024 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...
1025
                        continue;
1026
                    }
1027
1028 17
                    $haveScope = false;
1029 17
                    if (isset($mixin->parent->scope)) {
1030 2
                        $haveScope = true;
1031 2
                        $mixinParentEnv = $this->pushEnv($this->env);
1032 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...
1033
                    }
1034
1035 17
                    $haveArgs = false;
1036 17
                    if (isset($mixin->args)) {
1037 14
                        $haveArgs = true;
1038 14
                        $this->pushEnv($this->env);
1039 14
                        $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
1040
                    }
1041
1042 17
                    $oldParent = $mixin->parent;
1043 17
                    if ($mixin != $block) {
1044 17
                        $mixin->parent = $block;
1045
                    }
1046
1047 17
                    foreach ($this->sortProps($mixin->props) as $subProp) {
1048 17
                        if ($suffix !== null &&
1049 17
                            $subProp[0] === "assign" &&
1050 17
                            is_string($subProp[1]) &&
1051 17
                            $subProp[1]{0} != $this->vPrefix
1052
                        ) {
1053 1
                            $subProp[2] = [
1054 1
                                'list',
1055 1
                                ' ',
1056 1
                                [$subProp[2], ['keyword', $suffix]],
1057
                            ];
1058
                        }
1059
1060 17
                        $this->compileProp($subProp, $mixin, $out);
1061
                    }
1062
1063 17
                    $mixin->parent = $oldParent;
1064
1065 17
                    if ($haveArgs) {
1066 14
                        $this->popEnv();
1067
                    }
1068 17
                    if ($haveScope) {
1069 17
                        $this->popEnv();
1070
                    }
1071
                }
1072
1073 17
                break;
1074 5
            case 'raw':
1075
                $out->lines[] = $prop[1];
1076
                break;
1077 5
            case "directive":
1078 1
                list(, $name, $value) = $prop;
1079 1
                $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)) . ';';
1080 1
                break;
1081 4
            case "comment":
1082 1
                $out->lines[] = $prop[1];
1083 1
                break;
1084 3
            case "import":
1085 3
                list(, $importPath, $importId) = $prop;
1086 3
                $importPath = $this->reduce($importPath);
1087
1088 3
                $result = $this->tryImport($importPath, $block, $out);
1089
1090 3
                $this->env->addImports($importId, $result === false ?
1091 2
                    [false, "@import " . $this->compileValue($importPath) . ";"] :
1092 3
                    $result);
1093
1094 3
                break;
1095 3
            case "import_mixin":
1096 3
                list(, $importId) = $prop;
1097 3
                $import = $this->env->getImports($importId);
1098 3
                if ($import[0] === false) {
1099 3
                    if (isset($import[1])) {
1100 3
                        $out->lines[] = $import[1];
1101
                    }
1102
                } else {
1103 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...
1104 2
                    $this->compileImportedProps($bottom, $block, $out, $importDir);
1105
                }
1106
1107 3
                break;
1108
            default:
1109
                $block->parser->throwError("unknown op: {$prop[0]}\n", $block->count);
1110
        }
1111 38
    }
1112
1113
1114
	/**
1115
	 * Compiles a primitive value into a CSS property value.
1116
	 *
1117
	 * Values in lessphp are typed by being wrapped in arrays, their format is
1118
	 * typically:
1119
	 *
1120
	 *     array(type, contents [, additional_contents]*)
1121
	 *
1122
	 * The input is expected to be reduced. This function will not work on
1123
	 * things like expressions and variables.
1124
	 *
1125
	 * @param array $value
1126
	 * @param array $options
1127
	 *
1128
	 * @return string
1129
	 * @throws GeneralException
1130
	 */
1131 53
    public function compileValue(array $value, array $options = [])
1132
    {
1133
        try {
1134 53
            if (!isset($value[0])) {
1135
                throw new GeneralException('Missing value type');
1136
            }
1137
1138 53
            $options = array_replace([
1139 53
                'numberPrecision' => $this->numberPrecision,
1140 53
                'compressColors'  => ($this->formatter ? $this->formatter->getCompressColors() : false),
1141
            ], $options);
1142
1143 53
            $valueClass = \LesserPhp\Compiler\Value\AbstractValue::factory($this, $this->coerce, $options, $value);
1144
1145 52
            return $valueClass->getCompiled();
1146 1
        } catch (\UnexpectedValueException $e) {
1147 1
            throw new GeneralException($e->getMessage());
1148
        }
1149
    }
1150
1151
    /**
1152
     * Helper function to get arguments for color manipulation functions.
1153
     * takes a list that contains a color like thing and a percentage
1154
     *
1155
     * @param array $args
1156
     *
1157
     * @return array
1158
     */
1159 2
    public function colorArgs(array $args)
1160
    {
1161 2
        if ($args[0] !== 'list' || count($args[2]) < 2) {
1162 1
            return [['color', 0, 0, 0], 0];
1163
        }
1164 2
        list($color, $delta) = $args[2];
1165 2
        $color = $this->assertions->assertColor($color);
1166 2
        $delta = (float) $delta[1];
1167
1168 2
        return [$color, $delta];
1169
    }
1170
1171
    /**
1172
     * Convert the rgb, rgba, hsl color literals of function type
1173
     * as returned by the parser into values of color type.
1174
     *
1175
     * @param array $func
1176
     *
1177
     * @return bool|mixed
1178
     */
1179 24
    protected function funcToColor(array $func)
1180
    {
1181 24
        $fname = $func[1];
1182 24
        if ($func[2][0] !== 'list') {
1183 6
            return false;
1184
        } // need a list of arguments
1185
        /** @var array $rawComponents */
1186 24
        $rawComponents = $func[2][2];
1187
1188 24
        if ($fname === 'hsl' || $fname === 'hsla') {
1189 1
            $hsl = ['hsl'];
1190 1
            $i = 0;
1191 1
            foreach ($rawComponents as $c) {
1192 1
                $val = $this->reduce($c);
1193 1
                $val = isset($val[1]) ? (float) $val[1] : 0;
1194
1195 1
                if ($i === 0) {
1196 1
                    $clamp = 360;
1197 1
                } elseif ($i < 3) {
1198 1
                    $clamp = 100;
1199
                } else {
1200 1
                    $clamp = 1;
1201
                }
1202
1203 1
                $hsl[] = $this->converter->clamp($val, $clamp);
1204 1
                $i++;
1205
            }
1206
1207 1
            while (count($hsl) < 4) {
1208
                $hsl[] = 0;
1209
            }
1210
1211 1
            return $this->converter->toRGB($hsl);
1212
1213 24
        } elseif ($fname === 'rgb' || $fname === 'rgba') {
1214 4
            $components = [];
1215 4
            $i = 1;
1216 4
            foreach ($rawComponents as $c) {
1217 4
                $c = $this->reduce($c);
1218 4
                if ($i < 4) {
1219 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...
1220 1
                        $components[] = 255 * ($c[1] / 100);
1221
                    } else {
1222 4
                        $components[] = (float) $c[1];
1223
                    }
1224 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...
1225 4
                    if ($c[0] === "number" && $c[2] === "%") {
1226
                        $components[] = 1.0 * ($c[1] / 100);
1227
                    } else {
1228 4
                        $components[] = (float) $c[1];
1229
                    }
1230
                } else {
1231
                    break;
1232
                }
1233
1234 4
                $i++;
1235
            }
1236 4
            while (count($components) < 3) {
1237
                $components[] = 0;
1238
            }
1239 4
            array_unshift($components, 'color');
1240
1241 4
            return $this->fixColor($components);
1242
        }
1243
1244 23
        return false;
1245
    }
1246
1247
    /**
1248
     * @param array $value
1249
     * @param bool  $forExpression
1250
     *
1251
     * @return array|bool|mixed|null // <!-- dafuq?
1252
     */
1253 48
    public function reduce(array $value, $forExpression = false)
1254
    {
1255 48
        switch ($value[0]) {
1256 48
            case "interpolate":
1257 7
                $reduced = $this->reduce($value[1]);
1258 7
                $var = $this->compileValue($reduced);
1259 7
                $res = $this->reduce(["variable", $this->vPrefix . $var]);
1260
1261 7
                if ($res[0] === "raw_color") {
1262 1
                    $res = $this->coerce->coerceColor($res);
1263
                }
1264
1265 7
                if (empty($value[2])) {
1266 6
                    $res = $this->functions->e($res);
1267
                }
1268
1269 7
                return $res;
1270 48
            case "variable":
1271 28
                $key = $value[1];
1272 28
                if (is_array($key)) {
1273 1
                    $key = $this->reduce($key);
1274 1
                    $key = $this->vPrefix . $this->compileValue($this->functions->e($key));
1275
                }
1276
1277 28
                $seen =& $this->env->seenNames;
1278
1279 28
                if (!empty($seen[$key])) {
1280
                    $this->throwError("infinite loop detected: $key");
1281
                }
1282
1283 28
                $seen[$key] = true;
1284 28
                $out = $this->reduce($this->get($key));
1285 27
                $seen[$key] = false;
1286
1287 27
                return $out;
1288 47
            case "list":
1289 29
                foreach ($value[2] as &$item) {
1290 26
                    $item = $this->reduce($item, $forExpression);
1291
                }
1292
1293 29
                return $value;
1294 47
            case "expression":
1295 19
                return $this->evaluate($value);
1296 47
            case "string":
1297 26
                foreach ($value[2] as &$part) {
1298 26
                    if (is_array($part)) {
1299 11
                        $strip = $part[0] === "variable";
1300 11
                        $part = $this->reduce($part);
1301 11
                        if ($strip) {
1302 26
                            $part = $this->functions->e($part);
1303
                        }
1304
                    }
1305
                }
1306
1307 26
                return $value;
1308 45
            case "escape":
1309 4
                list(, $inner) = $value;
1310
1311 4
                return $this->functions->e($this->reduce($inner));
1312 45
            case "function":
1313 24
                $color = $this->funcToColor($value);
1314 24
                if ($color) {
1315 4
                    return $color;
1316
                }
1317
1318 23
                list(, $name, $args) = $value;
1319 23
                if ($name === "%") {
1320 1
                    $name = "_sprintf";
1321
                }
1322
1323
                // user functions
1324 23
                $f = null;
1325 23
                if (isset($this->libFunctions[$name]) && is_callable($this->libFunctions[$name])) {
1326 1
                    $f = $this->libFunctions[$name];
1327
                }
1328
1329 23
                $func = str_replace('-', '_', $name);
1330
1331 23
                if ($f !== null || method_exists($this->functions, $func)) {
1332 14
                    if ($args[0] === 'list') {
1333 14
                        $args = self::compressList($args[2], $args[1]);
1334
                    }
1335
1336 14
                    if ($f !== null) {
1337 1
                        $ret = $f($this->reduce($args, true), $this);
1338
                    } else {
1339 13
                        $ret = $this->functions->$func($this->reduce($args, true), $this);
1340
                    }
1341 9
                    if ($ret === null) {
1342
                        return [
1343 2
                            "string",
1344 2
                            "",
1345
                            [
1346 2
                                $name,
1347 2
                                "(",
1348 2
                                $args,
1349 2
                                ")",
1350
                            ],
1351
                        ];
1352
                    }
1353
1354
                    // convert to a typed value if the result is a php primitive
1355 8
                    if (is_numeric($ret)) {
1356 3
                        $ret = ['number', $ret, ""];
1357 7
                    } elseif (!is_array($ret)) {
1358 2
                        $ret = ['keyword', $ret];
1359
                    }
1360
1361 8
                    return $ret;
1362
                }
1363
1364
                // plain function, reduce args
1365 10
                $value[2] = $this->reduce($value[2]);
1366
1367 10
                return $value;
1368 40
            case "unary":
1369 6
                list(, $op, $exp) = $value;
1370 6
                $exp = $this->reduce($exp);
1371
1372 6
                if ($exp[0] === "number") {
1373
                    switch ($op) {
1374 6
                        case "+":
1375
                            return $exp;
1376 6
                        case "-":
1377 6
                            $exp[1] *= -1;
1378
1379 6
                            return $exp;
1380
                    }
1381
                }
1382
1383
                return ["string", "", [$op, $exp]];
1384
        }
1385
1386 40
        if ($forExpression) {
1387 22
            switch ($value[0]) {
1388 22
                case "keyword":
1389 5
                    $color = $this->coerce->coerceColor($value);
1390 5
                    if ($color !== null) {
1391 2
                        return $color;
1392
                    }
1393 5
                    break;
1394 22
                case "raw_color":
1395 6
                    return $this->coerce->coerceColor($value);
1396
            }
1397
        }
1398
1399 40
        return $value;
1400
    }
1401
1402
    /**
1403
     * turn list of length 1 into value type
1404
     *
1405
     * @param array $value
1406
     *
1407
     * @return array
1408
     */
1409 2
    protected function flattenList(array $value)
1410
    {
1411 2
        if ($value[0] === 'list' && count($value[2]) === 1) {
1412 2
            return $this->flattenList($value[2][0]);
1413
        }
1414
1415 2
        return $value;
1416
    }
1417
1418
    /**
1419
     * evaluate an expression
1420
     *
1421
     * @param array $exp
1422
     *
1423
     * @return array
1424
     */
1425 19
    protected function evaluate($exp)
1426
    {
1427 19
        list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
1428
1429 19
        $left = $this->reduce($left, true);
1430 19
        $right = $this->reduce($right, true);
1431
1432 19
        $leftColor = $this->coerce->coerceColor($left);
1433 19
        if ($leftColor !== null) {
1434 5
            $left = $leftColor;
1435
        }
1436
1437 19
        $rightColor = $this->coerce->coerceColor($right);
1438 19
        if ($rightColor !== null) {
1439 5
            $right = $rightColor;
1440
        }
1441
1442 19
        $ltype = $left[0];
1443 19
        $rtype = $right[0];
1444
1445
        // operators that work on all types
1446 19
        if ($op === "and") {
1447
            return $this->functions->toBool($left == self::$TRUE && $right == self::$TRUE);
1448
        }
1449
1450 19
        if ($op === "=") {
1451 1
            return $this->functions->toBool($this->equals($left, $right));
1452
        }
1453
1454 19
        $str = $this->stringConcatenate($left, $right);
1455 19
        if ($op === "+" && $str !== null) {
1456 2
            return $str;
1457
        }
1458
1459
        // type based operators
1460 17
        $fname = "op_${ltype}_${rtype}";
1461 17
        if (is_callable([$this, $fname])) {
1462 17
            $out = $this->$fname($op, $left, $right);
1463 17
            if ($out !== null) {
1464 17
                return $out;
1465
            }
1466
        }
1467
1468
        // make the expression look it did before being parsed
1469 1
        $paddedOp = $op;
1470 1
        if ($whiteBefore) {
1471 1
            $paddedOp = " " . $paddedOp;
1472
        }
1473 1
        if ($whiteAfter) {
1474 1
            $paddedOp .= " ";
1475
        }
1476
1477 1
        return ["string", "", [$left, $paddedOp, $right]];
1478
    }
1479
1480
    /**
1481
     * @param array $left
1482
     * @param array $right
1483
     *
1484
     * @return array|null
1485
     */
1486
    protected function stringConcatenate(array $left, array $right)
1487
    {
1488 19
        $strLeft = $this->coerce->coerceString($left);
1489 19
        if ($strLeft !== null) {
1490 2
            if ($right[0] === "string") {
1491 2
                $right[1] = "";
1492
            }
1493 2
            $strLeft[2][] = $right;
1494
1495 2
            return $strLeft;
1496
        }
1497
1498 18
        $strRight = $this->coerce->coerceString($right);
1499 18
        if ($strRight !== null) {
1500 1
            array_unshift($strRight[2], $left);
1501
1502 1
            return $strRight;
1503
        }
1504
1505 17
        return null;
1506
    }
1507
1508
1509
    /**
1510
     * make sure a color's components don't go out of bounds
1511
     *
1512
     * @param array $c
1513
     *
1514
     * @return mixed
1515
     */
1516
    public function fixColor(array $c)
1517
    {
1518 7
        foreach (range(1, 3) as $i) {
1519 7
            if ($c[$i] < 0) {
1520
                $c[$i] = 0;
1521
            }
1522 7
            if ($c[$i] > 255) {
1523 7
                $c[$i] = 255;
1524
            }
1525
        }
1526
1527 7
        return $c;
1528
    }
1529
1530
    /**
1531
     * @param string $op
1532
     * @param array  $lft
1533
     * @param array  $rgt
1534
     *
1535
     * @return array|null
1536
     * @throws \LesserPhp\Exception\GeneralException
1537
     */
1538
    protected function op_number_color($op, array $lft, array $rgt)
1539
    {
1540 1
        if ($op === '+' || $op === '*') {
1541 1
            return $this->op_color_number($op, $rgt, $lft);
1542
        }
1543
1544 1
        return null;
1545
    }
1546
1547
    /**
1548
     * @param string $op
1549
     * @param array  $lft
1550
     * @param array  $rgt
1551
     *
1552
     * @return array
1553
     * @throws \LesserPhp\Exception\GeneralException
1554
     */
1555
    protected function op_color_number($op, array $lft, array $rgt)
1556
    {
1557 2
        if ($rgt[0] === '%') {
1558
            $rgt[1] /= 100;
1559
        }
1560
1561 2
        return $this->op_color_color(
1562
            $op,
1563
            $lft,
1564 2
            array_fill(1, count($lft) - 1, $rgt[1])
1565
        );
1566
    }
1567
1568
    /**
1569
     * @param string $op
1570
     * @param        array
1571
     * $left
1572
     * @param array  $right
1573
     *
1574
     * @return array
1575
     * @throws \LesserPhp\Exception\GeneralException
1576
     */
1577
    protected function op_color_color($op, array $left, array $right)
1578
    {
1579 5
        $out = ['color'];
1580 5
        $max = count($left) > count($right) ? count($left) : count($right);
1581 5
        foreach (range(1, $max - 1) as $i) {
1582 5
            $lval = isset($left[$i]) ? $left[$i] : 0;
1583 5
            $rval = isset($right[$i]) ? $right[$i] : 0;
1584
            switch ($op) {
1585 5
                case '+':
1586 5
                    $out[] = $lval + $rval;
1587 5
                    break;
1588 1
                case '-':
1589 1
                    $out[] = $lval - $rval;
1590 1
                    break;
1591 1
                case '*':
1592 1
                    $out[] = $lval * $rval;
1593 1
                    break;
1594 1
                case '%':
1595 1
                    $out[] = $lval % $rval;
1596 1
                    break;
1597 1
                case '/':
1598 1
                    if ($rval == 0) {
1599
                        throw new GeneralException("evaluate error: can't divide by zero");
1600
                    }
1601 1
                    $out[] = $lval / $rval;
1602 1
                    break;
1603
                default:
1604 5
                    throw new GeneralException('evaluate error: color op number failed on op ' . $op);
1605
            }
1606
        }
1607
1608 5
        return $this->fixColor($out);
1609
    }
1610
1611
    /**
1612
     * operator on two numbers
1613
     *
1614
     * @param string $op
1615
     * @param array  $left
1616
     * @param array  $right
1617
     *
1618
     * @return array
1619
     * @throws \LesserPhp\Exception\GeneralException
1620
     */
1621
    protected function op_number_number($op, $left, $right)
1622
    {
1623 15
        $unit = empty($left[2]) ? $right[2] : $left[2];
1624
1625 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...
1626
        switch ($op) {
1627 15
            case '+':
1628 9
                $value = $left[1] + $right[1];
1629 9
                break;
1630 12
            case '*':
1631 8
                $value = $left[1] * $right[1];
1632 8
                break;
1633 10
            case '-':
1634 6
                $value = $left[1] - $right[1];
1635 6
                break;
1636 9
            case '%':
1637 1
                $value = $left[1] % $right[1];
1638 1
                break;
1639 8
            case '/':
1640 4
                if ($right[1] == 0) {
1641
                    throw new GeneralException('parse error: divide by zero');
1642
                }
1643 4
                $value = $left[1] / $right[1];
1644 4
                break;
1645 5
            case '<':
1646 1
                return $this->functions->toBool($left[1] < $right[1]);
1647 5
            case '>':
1648 3
                return $this->functions->toBool($left[1] > $right[1]);
1649 3
            case '>=':
1650 1
                return $this->functions->toBool($left[1] >= $right[1]);
1651 2
            case '=<':
1652 2
                return $this->functions->toBool($left[1] <= $right[1]);
1653
            default:
1654
                throw new GeneralException('parse error: unknown number operator: ' . $op);
1655
        }
1656
1657 13
        return ['number', $value, $unit];
1658
    }
1659
1660
    /**
1661
     * @param      $type
1662
     * @param null $selectors
1663
     *
1664
     * @return \stdClass
1665
     */
1666
    protected function makeOutputBlock($type, $selectors = null)
1667
    {
1668 49
        $b = new \stdClass();
1669 49
        $b->lines = [];
1670 49
        $b->children = [];
1671 49
        $b->selectors = $selectors;
1672 49
        $b->type = $type;
1673 49
        $b->parent = $this->scope;
1674
1675 49
        return $b;
1676
    }
1677
1678
    /**
1679
     * @param      $parent
1680
     * @param null $block
1681
     *
1682
     * @return \LesserPhp\NodeEnv
1683
     */
1684
    protected function pushEnv($parent, $block = null)
1685
    {
1686 49
        $e = new NodeEnv();
1687 49
        $e->setParent($parent);
1688 49
        $e->setBlock($block);
1689 49
        $e->setStore([]);
1690
1691 49
        $this->env = $e;
1692
1693 49
        return $e;
1694
    }
1695
1696
    /**
1697
     * pop something off the stack
1698
     *
1699
     * @return \LesserPhp\NodeEnv
1700
     */
1701
    protected function popEnv()
1702
    {
1703 40
        $old = $this->env;
1704 40
        $this->env = $this->env->getParent();
1705
1706 40
        return $old;
1707
    }
1708
1709
    /**
1710
     * set something in the current env
1711
     *
1712
     * @param $name
1713
     * @param $value
1714
     */
1715
    protected function set($name, $value)
1716
    {
1717 27
        $this->env->addStore($name, $value);
1718 27
    }
1719
1720
    /**
1721
     * get the highest occurrence entry for a name
1722
     *
1723
     * @param $name
1724
     *
1725
     * @return array
1726
     * @throws \LesserPhp\Exception\GeneralException
1727
     */
1728
    protected function get($name)
1729
    {
1730 28
        $current = $this->env;
1731
1732
        // track scope to evaluate
1733 28
        $scopeSecondary = [];
1734
1735 28
        $isArguments = $name === $this->vPrefix . 'arguments';
1736 28
        while ($current) {
1737 28
            if ($isArguments && count($current->getArguments()) > 0) {
1738 3
                return ['list', ' ', $current->getArguments()];
1739
            }
1740
1741 28
            if (isset($current->getStore()[$name])) {
1742 27
                return $current->getStore()[$name];
1743
            }
1744
            // has secondary scope?
1745 19
            if (isset($current->storeParent)) {
1746
                $scopeSecondary[] = $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...
1747
            }
1748
1749 19
            if ($current->getParent() !== null) {
1750 19
                $current = $current->getParent();
1751
            } else {
1752 1
                $current = null;
1753
            }
1754
        }
1755
1756 1
        while (count($scopeSecondary)) {
1757
            // pop one off
1758
            $current = array_shift($scopeSecondary);
1759
            while ($current) {
1760
                if ($isArguments && isset($current->arguments)) {
1761
                    return ['list', ' ', $current->arguments];
1762
                }
1763
1764
                if (isset($current->store[$name])) {
1765
                    return $current->store[$name];
1766
                }
1767
1768
                // has secondary scope?
1769
                if (isset($current->storeParent)) {
1770
                    $scopeSecondary[] = $current->storeParent;
1771
                }
1772
1773
                if (isset($current->parent)) {
1774
                    $current = $current->parent;
1775
                } else {
1776
                    $current = null;
1777
                }
1778
            }
1779
        }
1780
1781 1
        throw new GeneralException("variable $name is undefined");
1782
    }
1783
1784
    /**
1785
     * inject array of unparsed strings into environment as variables
1786
     *
1787
     * @param string[] $args
1788
     *
1789
     * @throws \LesserPhp\Exception\GeneralException
1790
     */
1791
    protected function injectVariables(array $args)
1792
    {
1793 1
        $this->pushEnv($this->env);
1794 1
        $parser = new Parser($this, __METHOD__);
1795 1
        foreach ($args as $name => $strValue) {
1796 1
            if ($name{0} !== '@') {
1797 1
                $name = '@' . $name;
1798
            }
1799 1
            $parser->count = 0;
1800 1
            $parser->buffer = (string) $strValue;
1801 1
            if (!$parser->propertyValue($value)) {
1802
                throw new GeneralException("failed to parse passed in variable $name: $strValue");
1803
            }
1804
1805 1
            $this->set($name, $value);
1806
        }
1807 1
    }
1808
1809
    /**
1810
     * @param string $string
1811
     * @param string $name
1812
     *
1813
     * @return string
1814
     * @throws \LesserPhp\Exception\GeneralException
1815
     */
1816
    public function compile($string, $name = null)
1817
    {
1818 49
        $locale = setlocale(LC_NUMERIC, 0);
1819 49
        setlocale(LC_NUMERIC, 'C');
1820
1821 49
        $this->parser = $this->makeParser($name);
1822 49
        $root = $this->parser->parse($string);
1823
1824 49
        $this->env = null;
1825 49
        $this->scope = null;
1826 49
        $this->allParsedFiles = [];
1827
1828 49
        $this->formatter = $this->newFormatter();
1829
1830 49
        if (!empty($this->registeredVars)) {
1831 1
            $this->injectVariables($this->registeredVars);
1832
        }
1833
1834 49
        $this->sourceParser = $this->parser; // used for error messages
1835 49
        $this->compileBlock($root);
1836
1837 38
        ob_start();
1838 38
        $this->formatter->block($this->scope);
1839 38
        $out = ob_get_clean();
1840 38
        setlocale(LC_NUMERIC, $locale);
1841
1842 38
        return $out;
1843
    }
1844
1845
    /**
1846
     * @param string $fname
1847
     * @param string $outFname
1848
     *
1849
     * @return int|string
1850
     * @throws \LesserPhp\Exception\GeneralException
1851
     */
1852
    public function compileFile($fname, $outFname = null)
1853
    {
1854 1
        if (!is_readable($fname)) {
1855
            throw new GeneralException('load error: failed to find ' . $fname);
1856
        }
1857
1858 1
        $pi = pathinfo($fname);
1859
1860 1
        $oldImport = $this->importDirs;
1861
1862 1
        $this->importDirs[] = $pi['dirname'] . '/';
1863
1864 1
        $this->addParsedFile($fname);
1865
1866 1
        $out = $this->compile(file_get_contents($fname), $fname);
1867
1868 1
        $this->importDirs = $oldImport;
1869
1870 1
        if ($outFname !== null) {
1871
            return file_put_contents($outFname, $out);
1872
        }
1873
1874 1
        return $out;
1875
    }
1876
1877
    /**
1878
     * Based on explicit input/output files does a full change check on cache before compiling.
1879
     *
1880
     * @param string  $in
1881
     * @param string  $out
1882
     * @param boolean $force
1883
     *
1884
     * @return string Compiled CSS results
1885
     * @throws GeneralException
1886
     */
1887
    public function checkedCachedCompile($in, $out, $force = false)
1888
    {
1889 1
        if (!is_file($in) || !is_readable($in)) {
1890
            throw new GeneralException('Invalid or unreadable input file specified.');
1891
        }
1892 1
        if (is_dir($out) || !is_writable(file_exists($out) ? $out : dirname($out))) {
1893
            throw new GeneralException('Invalid or unwritable output file specified.');
1894
        }
1895
1896 1
        $outMeta = $out . '.meta';
1897 1
        $metadata = null;
1898 1
        if (!$force && is_file($outMeta)) {
1899
            $metadata = unserialize(file_get_contents($outMeta));
1900
        }
1901
1902 1
        $output = $this->cachedCompile($metadata ?: $in);
1903
1904 1
        if (!$metadata || $metadata['updated'] != $output['updated']) {
1905 1
            $css = $output['compiled'];
1906 1
            unset($output['compiled']);
1907 1
            file_put_contents($out, $css);
1908 1
            file_put_contents($outMeta, serialize($output));
1909
        } else {
1910
            $css = file_get_contents($out);
1911
        }
1912
1913 1
        return $css;
1914
    }
1915
1916
    /**
1917
     * compile only if changed input has changed or output doesn't exist
1918
     *
1919
     * @param string $in
1920
     * @param string $out
1921
     *
1922
     * @return bool
1923
     * @throws \LesserPhp\Exception\GeneralException
1924
     */
1925
    public function checkedCompile($in, $out)
1926
    {
1927
        if (!is_file($out) || filemtime($in) > filemtime($out)) {
1928
            $this->compileFile($in, $out);
1929
1930
            return true;
1931
        }
1932
1933
        return false;
1934
    }
1935
1936
    /**
1937
     * Execute lessphp on a .less file or a lessphp cache structure
1938
     *
1939
     * The lessphp cache structure contains information about a specific
1940
     * less file having been parsed. It can be used as a hint for future
1941
     * calls to determine whether or not a rebuild is required.
1942
     *
1943
     * The cache structure contains two important keys that may be used
1944
     * externally:
1945
     *
1946
     * compiled: The final compiled CSS
1947
     * updated: The time (in seconds) the CSS was last compiled
1948
     *
1949
     * The cache structure is a plain-ol' PHP associative array and can
1950
     * be serialized and unserialized without a hitch.
1951
     *
1952
     * @param mixed $in    Input
1953
     * @param bool  $force Force rebuild?
1954
     *
1955
     * @return array lessphp cache structure
1956
     * @throws \LesserPhp\Exception\GeneralException
1957
     */
1958
    public function cachedCompile($in, $force = false)
1959
    {
1960
        // assume no root
1961 1
        $root = null;
1962
1963 1
        if (is_string($in)) {
1964 1
            $root = $in;
1965
        } elseif (is_array($in) && isset($in['root'])) {
1966
            if ($force || !isset($in['files'])) {
1967
                // If we are forcing a recompile or if for some reason the
1968
                // structure does not contain any file information we should
1969
                // specify the root to trigger a rebuild.
1970
                $root = $in['root'];
1971
            } elseif (isset($in['files']) && is_array($in['files'])) {
1972
                foreach ($in['files'] as $fname => $ftime) {
1973
                    if (!file_exists($fname) || filemtime($fname) > $ftime) {
1974
                        // One of the files we knew about previously has changed
1975
                        // so we should look at our incoming root again.
1976
                        $root = $in['root'];
1977
                        break;
1978
                    }
1979
                }
1980
            }
1981
        } else {
1982
            // TODO: Throw an exception? We got neither a string nor something
1983
            // that looks like a compatible lessphp cache structure.
1984
            return null;
1985
        }
1986
1987 1
        if ($root !== null) {
1988
            // If we have a root value which means we should rebuild.
1989 1
            $out = [];
1990 1
            $out['root'] = $root;
1991 1
            $out['compiled'] = $this->compileFile($root);
1992 1
            $out['files'] = $this->allParsedFiles;
1993 1
            $out['updated'] = time();
1994
1995 1
            return $out;
1996
        } else {
1997
            // No changes, pass back the structure
1998
            // we were given initially.
1999
            return $in;
2000
        }
2001
    }
2002
2003
    /**
2004
     * parse and compile buffer
2005
     * This is deprecated
2006
     *
2007
     * @param null $str
2008
     * @param null $initialVariables
2009
     *
2010
     * @return int|string
2011
     * @throws \LesserPhp\Exception\GeneralException
2012
     * @deprecated
2013
     */
2014
    public function parse($str = null, $initialVariables = null)
2015
    {
2016 37
        if (is_array($str)) {
2017
            $initialVariables = $str;
2018
            $str = null;
2019
        }
2020
2021 37
        $oldVars = $this->registeredVars;
2022 37
        if ($initialVariables !== null) {
2023 1
            $this->setVariables($initialVariables);
2024
        }
2025
2026 37
        if ($str === null) {
2027
            throw new GeneralException('nothing to parse');
2028
        } else {
2029 37
            $out = $this->compile($str);
2030
        }
2031
2032 37
        $this->registeredVars = $oldVars;
2033
2034 37
        return $out;
2035
    }
2036
2037
    /**
2038
     * @param string $name
2039
     *
2040
     * @return \LesserPhp\Parser
2041
     */
2042
    protected function makeParser($name)
2043
    {
2044 49
        $parser = new Parser($this, $name);
2045 49
        $parser->setWriteComments($this->preserveComments);
2046
2047 49
        return $parser;
2048
    }
2049
2050
    /**
2051
     * @param string $name
2052
     */
2053
    public function setFormatter($name)
2054
    {
2055 1
        $this->formatterName = $name;
2056 1
    }
2057
2058
	public function setFormatterClass($formatter)
2059
	{
2060 14
		$this->formatter = $formatter;
2061 14
    }
2062
2063
    /**
2064
     * @return \LesserPhp\Formatter\FormatterInterface
2065
     */
2066
    protected function newFormatter()
2067
    {
2068 49
        $className = 'Lessjs';
2069 49
        if (!empty($this->formatterName)) {
2070 1
            if (!is_string($this->formatterName)) {
2071
                return $this->formatterName;
2072
            }
2073 1
            $className = $this->formatterName;
2074
        }
2075
2076 49
        $className = '\LesserPhp\Formatter\\' . $className;
2077
2078 49
        return new $className;
2079
    }
2080
2081
    /**
2082
     * @param bool $preserve
2083
     */
2084
    public function setPreserveComments($preserve)
2085
    {
2086 1
        $this->preserveComments = $preserve;
2087 1
    }
2088
2089
    /**
2090
     * @param string   $name
2091
     * @param callable $func
2092
     */
2093
    public function registerFunction($name, callable $func)
2094
    {
2095 1
        $this->libFunctions[$name] = $func;
2096 1
    }
2097
2098
    /**
2099
     * @param string $name
2100
     */
2101
    public function unregisterFunction($name)
2102
    {
2103 1
        unset($this->libFunctions[$name]);
2104 1
    }
2105
2106
    /**
2107
     * @param array $variables
2108
     */
2109
    public function setVariables(array $variables)
2110
    {
2111 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...
2112 1
    }
2113
2114
    /**
2115
     * @param $name
2116
     */
2117
    public function unsetVariable($name)
2118
    {
2119
        unset($this->registeredVars[$name]);
2120
    }
2121
2122
    /**
2123
     * @param string[] $dirs
2124
     */
2125
    public function setImportDirs(array $dirs)
2126
    {
2127 38
        $this->importDirs = $dirs;
2128 38
    }
2129
2130
    /**
2131
     * @param string $dir
2132
     */
2133
    public function addImportDir($dir)
2134
    {
2135
        $this->importDirs[] = $dir;
2136
    }
2137
2138
    /**
2139
     * @return string[]
2140
     */
2141
    public function getImportDirs()
2142
    {
2143 4
        return $this->importDirs;
2144
    }
2145
2146
    /**
2147
     * @param string $file
2148
     */
2149
    public function addParsedFile($file)
2150
    {
2151 2
        $this->allParsedFiles[realpath($file)] = filemtime($file);
2152 2
    }
2153
2154
    /**
2155
     * Uses the current value of $this->count to show line and line number
2156
     *
2157
     * @param string $msg
2158
     *
2159
     * @throws GeneralException
2160
     */
2161
    public function throwError($msg = null)
2162
    {
2163
        if ($this->sourceLoc >= 0) {
2164
            $this->sourceParser->throwError($msg, $this->sourceLoc);
2165
        }
2166
        throw new GeneralException($msg);
2167
    }
2168
2169
    /**
2170
     * compile file $in to file $out if $in is newer than $out
2171
     * returns true when it compiles, false otherwise
2172
     *
2173
     * @param                          $in
2174
     * @param                          $out
2175
     * @param \LesserPhp\Compiler|null $less
2176
     *
2177
     * @return bool
2178
     * @throws \LesserPhp\Exception\GeneralException
2179
     */
2180
    public static function ccompile($in, $out, Compiler $less = null)
2181
    {
2182
        if ($less === null) {
2183
            $less = new self;
2184
        }
2185
2186
        return $less->checkedCompile($in, $out);
2187
    }
2188
2189
    /**
2190
     * @param                          $in
2191
     * @param bool                     $force
2192
     * @param \LesserPhp\Compiler|null $less
2193
     *
2194
     * @return array
2195
     * @throws \LesserPhp\Exception\GeneralException
2196
     */
2197
    public static function cexecute($in, $force = false, Compiler $less = null)
2198
    {
2199
        if ($less === null) {
2200
            $less = new self;
2201
        }
2202
2203
        return $less->cachedCompile($in, $force);
2204
    }
2205
2206
    /**
2207
     * prefix of abstract properties
2208
     *
2209
     * @return string
2210
     */
2211
    public function getVPrefix()
2212
    {
2213 49
        return $this->vPrefix;
2214
    }
2215
2216
    /**
2217
     * prefix of abstract blocks
2218
     *
2219
     * @return string
2220
     */
2221
    public function getMPrefix()
2222
    {
2223 46
        return $this->mPrefix;
2224
    }
2225
2226
    /**
2227
     * @return string
2228
     */
2229
    public function getParentSelector()
2230
    {
2231 3
        return $this->parentSelector;
2232
    }
2233
2234
    /**
2235
     * @param int $numberPresicion
2236
     */
2237
    protected function setNumberPrecision($numberPresicion = null)
2238
    {
2239
        $this->numberPrecision = $numberPresicion;
2240
    }
2241
2242
    /**
2243
     * @return \LesserPhp\Library\Coerce
2244
     */
2245
    protected function getCoerce()
2246
    {
2247
        return $this->coerce;
2248
    }
2249
2250
    public function setImportDisabled()
2251
    {
2252 1
        $this->importDisabled = true;
2253 1
    }
2254
2255
    /**
2256
     * @return bool
2257
     */
2258
    public function isImportDisabled()
2259
    {
2260 3
        return $this->importDisabled;
2261
    }
2262
}
2263