Completed
Push — master ( fd2bc2...1dc9e9 )
by Marcus
03:01
created

Compiler::fileExists()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 2
CRAP Score 1

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 2
cts 2
cp 1
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 1
crap 1
1
<?php
2
3
namespace LesserPhp;
4
5
/**
6
 * lesserphp
7
 * https://www.maswaba.de/lesserphp
8
 *
9
 * LESS CSS compiler, adapted from http://lesscss.org
10
 *
11
 * Copyright 2013, Leaf Corcoran <[email protected]>
12
 * Copyright 2016, Marcus Schwarz <[email protected]>
13
 * Licensed under MIT or GPLv3, see LICENSE
14
 * @package LesserPhp
15
 */
16
use LesserPhp\Color\Converter;
17
use LesserPhp\Exception\GeneralException;
18
use LesserPhp\Library\Assertions;
19
use LesserPhp\Library\Coerce;
20
use LesserPhp\Library\Functions;
21
22
/**
23
 * The LESS compiler and parser.
24
 *
25
 * Converting LESS to CSS is a three stage process. The incoming file is parsed
26
 * by `lessc_parser` into a syntax tree, then it is compiled into another tree
27
 * representing the CSS structure by `lessc`. The CSS tree is fed into a
28
 * formatter, like `lessc_formatter` which then outputs CSS as a string.
29
 *
30
 * During the first compile, all values are *reduced*, which means that their
31
 * types are brought to the lowest form before being dump as strings. This
32
 * handles math equations, variable dereferences, and the like.
33
 *
34
 * The `parse` function of `lessc` is the entry point.
35
 *
36
 * In summary:
37
 *
38
 * The `lessc` class creates an instance of the parser, feeds it LESS code,
39
 * then transforms the resulting tree to a CSS tree. This class also holds the
40
 * evaluation context, such as all available mixins and variables at any given
41
 * time.
42
 *
43
 * The `lessc_parser` class is only concerned with parsing its input.
44
 *
45
 * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
46
 * handling things like indentation.
47
 */
48
class Compiler
49
{
50
51
    const VERSION = 'v0.5.1';
52
53
    static public $TRUE = ['keyword', 'true'];
54
    static public $FALSE = ['keyword', 'false'];
55
56
    /**
57
     * @var callable[]
58
     */
59
    private $libFunctions = [];
60
61
    /**
62
     * @var string[]
63
     */
64
    private $registeredVars = [];
65
66
    /**
67
     * @var bool
68
     */
69
    protected $preserveComments = false;
70
71
    /**
72
     * @var string $vPrefix prefix of abstract properties
73
     */
74
    private $vPrefix = '@';
75
76
    /**
77
     * @var string $mPrefix prefix of abstract blocks
78
     */
79
    private $mPrefix = '$';
80
81
    /**
82
     * @var string
83
     */
84
    private $parentSelector = '&';
85
86
    /**
87
     * @var bool $importDisabled disable @import
88
     */
89
    private $importDisabled = false;
90
91
    /**
92
     * @var string[]
93
     */
94
    private $importDirs = [];
95
96
    /**
97
     * @var int
98
     */
99
    private $numberPrecision;
100
101
    /**
102
     * @var string[]
103
     */
104
    private $allParsedFiles = [];
105
106
    /**
107
     * set to the parser that generated the current line when compiling
108
     * so we know how to create error messages
109
     * @var \LesserPhp\Parser
110
     */
111
    private $sourceParser;
112
113
    /**
114
     * @var integer $sourceLoc Lines of Code
115
     */
116
    private $sourceLoc;
117
118
    /**
119
     * @var int $nextImportId uniquely identify imports
120
     */
121
    static private $nextImportId = 0;
122
123
    /**
124
     * @var Parser
125
     */
126
    private $parser;
127
128
    /**
129
     * @var \LesserPhp\Formatter\FormatterInterface
130
     */
131
    private $formatter;
132
133
    /**
134
     * @var \LesserPhp\NodeEnv What's the meaning of "env" in this context?
135
     */
136
    private $env;
137
138
    /**
139
     * @var \LesserPhp\Library\Coerce
140
     */
141
    private $coerce;
142
143
    /**
144
     * @var \LesserPhp\Library\Assertions
145
     */
146
    private $assertions;
147
148
    /**
149
     * @var \LesserPhp\Library\Functions
150
     */
151
    private $functions;
152
153
    /**
154
     * @var mixed what's this exactly?
155
     */
156
    private $scope;
157
158
    /**
159
     * @var string
160
     */
161
    private $formatterName;
162
163
    /**
164
     * @var \LesserPhp\Color\Converter
165
     */
166
    private $converter;
167
168
    /**
169
     * Constructor.
170
     *
171
     * Hardwires dependencies for now
172
     */
173 49
    public function __construct()
174
    {
175 49
        $this->coerce = new Coerce();
176 49
        $this->assertions = new Assertions($this->coerce);
177 49
        $this->converter = new Converter();
178 49
        $this->functions = new Functions($this->assertions, $this->coerce, $this, $this->converter);
179 49
    }
180
181
    /**
182
     * @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
     *
1127
     * @return string
1128
     * @throws \LesserPhp\Exception\GeneralException
1129
     */
1130 38
    public function compileValue(array $value)
1131
    {
1132 38
        switch ($value[0]) {
1133 38
            case 'list':
1134
                // [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...
1135
                // [2] - array of values
1136 21
                return implode($value[1], array_map([$this, 'compileValue'], $value[2]));
1137 38
            case 'raw_color':
1138 8
                if ($this->formatter->getCompressColors()) {
1139
                    return $this->compileValue($this->coerce->coerceColor($value));
1140
                }
1141
1142 8
                return $value[1];
1143 38
            case 'keyword':
1144
                // [1] - the keyword
1145 32
                return $value[1];
1146 36
            case 'number':
1147 32
                list(, $num, $unit) = $value;
1148
                // [1] - the number
1149
                // [2] - the unit
1150 32
                if ($this->numberPrecision !== null) {
1151
                    $num = round($num, $this->numberPrecision);
1152
                }
1153
1154 32
                return $num . $unit;
1155 29
            case 'string':
1156
                // [1] - contents of string (includes quotes)
1157 24
                list(, $delim, $content) = $value;
1158 24
                foreach ($content as &$part) {
1159 24
                    if (is_array($part)) {
1160 24
                        $part = $this->compileValue($part);
1161
                    }
1162
                }
1163
1164 24
                return $delim . implode($content) . $delim;
1165 17
            case 'color':
1166
                // [1] - red component (either number or a %)
1167
                // [2] - green component
1168
                // [3] - blue component
1169
                // [4] - optional alpha component
1170 8
                list(, $r, $g, $b) = $value;
1171 8
                $r = round($r);
1172 8
                $g = round($g);
1173 8
                $b = round($b);
1174
1175 8
                if (count($value) === 5 && $value[4] != 1) { // rgba
1176 3
                    return 'rgba(' . $r . ',' . $g . ',' . $b . ',' . $value[4] . ')';
1177
                }
1178
1179 8
                $h = sprintf("#%02x%02x%02x", $r, $g, $b);
1180
1181 8
                if ($this->formatter->getCompressColors()) {
1182
                    // Converting hex color to short notation (e.g. #003399 to #039)
1183 1
                    if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
1184 1
                        $h = '#' . $h[1] . $h[3] . $h[5];
1185
                    }
1186
                }
1187
1188 8
                return $h;
1189
1190 10
            case 'function':
1191 10
                list(, $name, $args) = $value;
1192
1193 10
                return $name . '(' . $this->compileValue($args) . ')';
1194
            default: // assumed to be unit
1195
                throw new GeneralException('unknown value type: ' . $value[0]);
1196
        }
1197
    }
1198
1199
    /**
1200
     * Helper function to get arguments for color manipulation functions.
1201
     * takes a list that contains a color like thing and a percentage
1202
     *
1203
     * @param array $args
1204
     *
1205
     * @return array
1206
     */
1207 2
    public function colorArgs(array $args)
1208
    {
1209 2
        if ($args[0] !== 'list' || count($args[2]) < 2) {
1210 1
            return [['color', 0, 0, 0], 0];
1211
        }
1212 2
        list($color, $delta) = $args[2];
1213 2
        $color = $this->assertions->assertColor($color);
1214 2
        $delta = (float) $delta[1];
1215
1216 2
        return [$color, $delta];
1217
    }
1218
1219
    /**
1220
     * Convert the rgb, rgba, hsl color literals of function type
1221
     * as returned by the parser into values of color type.
1222
     *
1223
     * @param array $func
1224
     *
1225
     * @return bool|mixed
1226
     */
1227 24
    protected function funcToColor(array $func)
1228
    {
1229 24
        $fname = $func[1];
1230 24
        if ($func[2][0] !== 'list') {
1231 6
            return false;
1232
        } // need a list of arguments
1233
        /** @var array $rawComponents */
1234 24
        $rawComponents = $func[2][2];
1235
1236 24
        if ($fname === 'hsl' || $fname === 'hsla') {
1237 1
            $hsl = ['hsl'];
1238 1
            $i = 0;
1239 1
            foreach ($rawComponents as $c) {
1240 1
                $val = $this->reduce($c);
1241 1
                $val = isset($val[1]) ? (float) $val[1] : 0;
1242
1243 1
                if ($i === 0) {
1244 1
                    $clamp = 360;
1245 1
                } elseif ($i < 3) {
1246 1
                    $clamp = 100;
1247
                } else {
1248 1
                    $clamp = 1;
1249
                }
1250
1251 1
                $hsl[] = $this->converter->clamp($val, $clamp);
1252 1
                $i++;
1253
            }
1254
1255 1
            while (count($hsl) < 4) {
1256
                $hsl[] = 0;
1257
            }
1258
1259 1
            return $this->converter->toRGB($hsl);
1260
1261 24
        } elseif ($fname === 'rgb' || $fname === 'rgba') {
1262 4
            $components = [];
1263 4
            $i = 1;
1264 4
            foreach ($rawComponents as $c) {
1265 4
                $c = $this->reduce($c);
1266 4
                if ($i < 4) {
1267 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...
1268 1
                        $components[] = 255 * ($c[1] / 100);
1269
                    } else {
1270 4
                        $components[] = (float) $c[1];
1271
                    }
1272 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...
1273 4
                    if ($c[0] === "number" && $c[2] === "%") {
1274
                        $components[] = 1.0 * ($c[1] / 100);
1275
                    } else {
1276 4
                        $components[] = (float) $c[1];
1277
                    }
1278
                } else {
1279
                    break;
1280
                }
1281
1282 4
                $i++;
1283
            }
1284 4
            while (count($components) < 3) {
1285
                $components[] = 0;
1286
            }
1287 4
            array_unshift($components, 'color');
1288
1289 4
            return $this->fixColor($components);
1290
        }
1291
1292 23
        return false;
1293
    }
1294
1295
    /**
1296
     * @param array $value
1297
     * @param bool  $forExpression
1298
     *
1299
     * @return array|bool|mixed|null // <!-- dafuq?
1300
     */
1301 48
    public function reduce(array $value, $forExpression = false)
1302
    {
1303 48
        switch ($value[0]) {
1304 48
            case "interpolate":
1305 7
                $reduced = $this->reduce($value[1]);
1306 7
                $var = $this->compileValue($reduced);
1307 7
                $res = $this->reduce(["variable", $this->vPrefix . $var]);
1308
1309 7
                if ($res[0] === "raw_color") {
1310 1
                    $res = $this->coerce->coerceColor($res);
1311
                }
1312
1313 7
                if (empty($value[2])) {
1314 6
                    $res = $this->functions->e($res);
1315
                }
1316
1317 7
                return $res;
1318 48
            case "variable":
1319 28
                $key = $value[1];
1320 28
                if (is_array($key)) {
1321 1
                    $key = $this->reduce($key);
1322 1
                    $key = $this->vPrefix . $this->compileValue($this->functions->e($key));
1323
                }
1324
1325 28
                $seen =& $this->env->seenNames;
1326
1327 28
                if (!empty($seen[$key])) {
1328
                    $this->throwError("infinite loop detected: $key");
1329
                }
1330
1331 28
                $seen[$key] = true;
1332 28
                $out = $this->reduce($this->get($key));
1333 27
                $seen[$key] = false;
1334
1335 27
                return $out;
1336 47
            case "list":
1337 29
                foreach ($value[2] as &$item) {
1338 26
                    $item = $this->reduce($item, $forExpression);
1339
                }
1340
1341 29
                return $value;
1342 47
            case "expression":
1343 19
                return $this->evaluate($value);
1344 47
            case "string":
1345 26
                foreach ($value[2] as &$part) {
1346 26
                    if (is_array($part)) {
1347 11
                        $strip = $part[0] === "variable";
1348 11
                        $part = $this->reduce($part);
1349 11
                        if ($strip) {
1350 26
                            $part = $this->functions->e($part);
1351
                        }
1352
                    }
1353
                }
1354
1355 26
                return $value;
1356 45
            case "escape":
1357 4
                list(, $inner) = $value;
1358
1359 4
                return $this->functions->e($this->reduce($inner));
1360 45
            case "function":
1361 24
                $color = $this->funcToColor($value);
1362 24
                if ($color) {
1363 4
                    return $color;
1364
                }
1365
1366 23
                list(, $name, $args) = $value;
1367 23
                if ($name === "%") {
1368 1
                    $name = "_sprintf";
1369
                }
1370
1371
                // user functions
1372 23
                $f = null;
1373 23
                if (isset($this->libFunctions[$name]) && is_callable($this->libFunctions[$name])) {
1374 1
                    $f = $this->libFunctions[$name];
1375
                }
1376
1377 23
                $func = str_replace('-', '_', $name);
1378
1379 23
                if ($f !== null || method_exists($this->functions, $func)) {
1380 14
                    if ($args[0] === 'list') {
1381 14
                        $args = self::compressList($args[2], $args[1]);
1382
                    }
1383
1384 14
                    if ($f !== null) {
1385 1
                        $ret = $f($this->reduce($args, true), $this);
1386
                    } else {
1387 13
                        $ret = $this->functions->$func($this->reduce($args, true), $this);
1388
                    }
1389 9
                    if ($ret === null) {
1390
                        return [
1391 2
                            "string",
1392 2
                            "",
1393
                            [
1394 2
                                $name,
1395 2
                                "(",
1396 2
                                $args,
1397 2
                                ")",
1398
                            ],
1399
                        ];
1400
                    }
1401
1402
                    // convert to a typed value if the result is a php primitive
1403 8
                    if (is_numeric($ret)) {
1404 3
                        $ret = ['number', $ret, ""];
1405 7
                    } elseif (!is_array($ret)) {
1406 2
                        $ret = ['keyword', $ret];
1407
                    }
1408
1409 8
                    return $ret;
1410
                }
1411
1412
                // plain function, reduce args
1413 10
                $value[2] = $this->reduce($value[2]);
1414
1415 10
                return $value;
1416 40
            case "unary":
1417 6
                list(, $op, $exp) = $value;
1418 6
                $exp = $this->reduce($exp);
1419
1420 6
                if ($exp[0] === "number") {
1421
                    switch ($op) {
1422 6
                        case "+":
1423
                            return $exp;
1424 6
                        case "-":
1425 6
                            $exp[1] *= -1;
1426
1427 6
                            return $exp;
1428
                    }
1429
                }
1430
1431
                return ["string", "", [$op, $exp]];
1432
        }
1433
1434 40
        if ($forExpression) {
1435 22
            switch ($value[0]) {
1436 22
                case "keyword":
1437 5
                    $color = $this->coerce->coerceColor($value);
1438 5
                    if ($color !== null) {
1439 2
                        return $color;
1440
                    }
1441 5
                    break;
1442 22
                case "raw_color":
1443 6
                    return $this->coerce->coerceColor($value);
1444
            }
1445
        }
1446
1447 40
        return $value;
1448
    }
1449
1450
    /**
1451
     * turn list of length 1 into value type
1452
     *
1453
     * @param array $value
1454
     *
1455
     * @return array
1456
     */
1457 2
    protected function flattenList(array $value)
1458
    {
1459 2
        if ($value[0] === 'list' && count($value[2]) === 1) {
1460 2
            return $this->flattenList($value[2][0]);
1461
        }
1462
1463 2
        return $value;
1464
    }
1465
1466
    /**
1467
     * evaluate an expression
1468
     *
1469
     * @param array $exp
1470
     *
1471
     * @return array
1472
     */
1473 19
    protected function evaluate($exp)
1474
    {
1475 19
        list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
1476
1477 19
        $left = $this->reduce($left, true);
1478 19
        $right = $this->reduce($right, true);
1479
1480 19
        $leftColor = $this->coerce->coerceColor($left);
1481 19
        if ($leftColor !== null) {
1482 5
            $left = $leftColor;
1483
        }
1484
1485 19
        $rightColor = $this->coerce->coerceColor($right);
1486 19
        if ($rightColor !== null) {
1487 5
            $right = $rightColor;
1488
        }
1489
1490 19
        $ltype = $left[0];
1491 19
        $rtype = $right[0];
1492
1493
        // operators that work on all types
1494 19
        if ($op === "and") {
1495
            return $this->functions->toBool($left == self::$TRUE && $right == self::$TRUE);
1496
        }
1497
1498 19
        if ($op === "=") {
1499 1
            return $this->functions->toBool($this->equals($left, $right));
1500
        }
1501
1502 19
        $str = $this->stringConcatenate($left, $right);
1503 19
        if ($op === "+" && $str !== null) {
1504 2
            return $str;
1505
        }
1506
1507
        // type based operators
1508 17
        $fname = "op_${ltype}_${rtype}";
1509 17
        if (is_callable([$this, $fname])) {
1510 17
            $out = $this->$fname($op, $left, $right);
1511 17
            if ($out !== null) {
1512 17
                return $out;
1513
            }
1514
        }
1515
1516
        // make the expression look it did before being parsed
1517 1
        $paddedOp = $op;
1518 1
        if ($whiteBefore) {
1519 1
            $paddedOp = " " . $paddedOp;
1520
        }
1521 1
        if ($whiteAfter) {
1522 1
            $paddedOp .= " ";
1523
        }
1524
1525 1
        return ["string", "", [$left, $paddedOp, $right]];
1526
    }
1527
1528
    /**
1529
     * @param array $left
1530
     * @param array $right
1531
     *
1532
     * @return array|null
1533
     */
1534
    protected function stringConcatenate(array $left, array $right)
1535
    {
1536 19
        $strLeft = $this->coerce->coerceString($left);
1537 19
        if ($strLeft !== null) {
1538 2
            if ($right[0] === "string") {
1539 2
                $right[1] = "";
1540
            }
1541 2
            $strLeft[2][] = $right;
1542
1543 2
            return $strLeft;
1544
        }
1545
1546 18
        $strRight = $this->coerce->coerceString($right);
1547 18
        if ($strRight !== null) {
1548 1
            array_unshift($strRight[2], $left);
1549
1550 1
            return $strRight;
1551
        }
1552
1553 17
        return null;
1554
    }
1555
1556
1557
    /**
1558
     * make sure a color's components don't go out of bounds
1559
     *
1560
     * @param array $c
1561
     *
1562
     * @return mixed
1563
     */
1564
    public function fixColor(array $c)
1565
    {
1566 7
        foreach (range(1, 3) as $i) {
1567 7
            if ($c[$i] < 0) {
1568
                $c[$i] = 0;
1569
            }
1570 7
            if ($c[$i] > 255) {
1571 7
                $c[$i] = 255;
1572
            }
1573
        }
1574
1575 7
        return $c;
1576
    }
1577
1578
    /**
1579
     * @param string $op
1580
     * @param array  $lft
1581
     * @param array  $rgt
1582
     *
1583
     * @return array|null
1584
     * @throws \LesserPhp\Exception\GeneralException
1585
     */
1586
    protected function op_number_color($op, array $lft, array $rgt)
1587
    {
1588 1
        if ($op === '+' || $op === '*') {
1589 1
            return $this->op_color_number($op, $rgt, $lft);
1590
        }
1591
1592 1
        return null;
1593
    }
1594
1595
    /**
1596
     * @param string $op
1597
     * @param array  $lft
1598
     * @param array  $rgt
1599
     *
1600
     * @return array
1601
     * @throws \LesserPhp\Exception\GeneralException
1602
     */
1603
    protected function op_color_number($op, array $lft, array $rgt)
1604
    {
1605 2
        if ($rgt[0] === '%') {
1606
            $rgt[1] /= 100;
1607
        }
1608
1609 2
        return $this->op_color_color(
1610
            $op,
1611
            $lft,
1612 2
            array_fill(1, count($lft) - 1, $rgt[1])
1613
        );
1614
    }
1615
1616
    /**
1617
     * @param string $op
1618
     * @param        array
1619
     * $left
1620
     * @param array  $right
1621
     *
1622
     * @return array
1623
     * @throws \LesserPhp\Exception\GeneralException
1624
     */
1625
    protected function op_color_color($op, array $left, array $right)
1626
    {
1627 5
        $out = ['color'];
1628 5
        $max = count($left) > count($right) ? count($left) : count($right);
1629 5
        foreach (range(1, $max - 1) as $i) {
1630 5
            $lval = isset($left[$i]) ? $left[$i] : 0;
1631 5
            $rval = isset($right[$i]) ? $right[$i] : 0;
1632
            switch ($op) {
1633 5
                case '+':
1634 5
                    $out[] = $lval + $rval;
1635 5
                    break;
1636 1
                case '-':
1637 1
                    $out[] = $lval - $rval;
1638 1
                    break;
1639 1
                case '*':
1640 1
                    $out[] = $lval * $rval;
1641 1
                    break;
1642 1
                case '%':
1643 1
                    $out[] = $lval % $rval;
1644 1
                    break;
1645 1
                case '/':
1646 1
                    if ($rval == 0) {
1647
                        throw new GeneralException("evaluate error: can't divide by zero");
1648
                    }
1649 1
                    $out[] = $lval / $rval;
1650 1
                    break;
1651
                default:
1652 5
                    throw new GeneralException('evaluate error: color op number failed on op ' . $op);
1653
            }
1654
        }
1655
1656 5
        return $this->fixColor($out);
1657
    }
1658
1659
    /**
1660
     * operator on two numbers
1661
     *
1662
     * @param string $op
1663
     * @param array  $left
1664
     * @param array  $right
1665
     *
1666
     * @return array
1667
     * @throws \LesserPhp\Exception\GeneralException
1668
     */
1669
    protected function op_number_number($op, $left, $right)
1670
    {
1671 15
        $unit = empty($left[2]) ? $right[2] : $left[2];
1672
1673 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...
1674
        switch ($op) {
1675 15
            case '+':
1676 9
                $value = $left[1] + $right[1];
1677 9
                break;
1678 12
            case '*':
1679 8
                $value = $left[1] * $right[1];
1680 8
                break;
1681 10
            case '-':
1682 6
                $value = $left[1] - $right[1];
1683 6
                break;
1684 9
            case '%':
1685 1
                $value = $left[1] % $right[1];
1686 1
                break;
1687 8
            case '/':
1688 4
                if ($right[1] == 0) {
1689
                    throw new GeneralException('parse error: divide by zero');
1690
                }
1691 4
                $value = $left[1] / $right[1];
1692 4
                break;
1693 5
            case '<':
1694 1
                return $this->functions->toBool($left[1] < $right[1]);
1695 5
            case '>':
1696 3
                return $this->functions->toBool($left[1] > $right[1]);
1697 3
            case '>=':
1698 1
                return $this->functions->toBool($left[1] >= $right[1]);
1699 2
            case '=<':
1700 2
                return $this->functions->toBool($left[1] <= $right[1]);
1701
            default:
1702
                throw new GeneralException('parse error: unknown number operator: ' . $op);
1703
        }
1704
1705 13
        return ['number', $value, $unit];
1706
    }
1707
1708
    /**
1709
     * @param      $type
1710
     * @param null $selectors
1711
     *
1712
     * @return \stdClass
1713
     */
1714
    protected function makeOutputBlock($type, $selectors = null)
1715
    {
1716 49
        $b = new \stdClass();
1717 49
        $b->lines = [];
1718 49
        $b->children = [];
1719 49
        $b->selectors = $selectors;
1720 49
        $b->type = $type;
1721 49
        $b->parent = $this->scope;
1722
1723 49
        return $b;
1724
    }
1725
1726
    /**
1727
     * @param      $parent
1728
     * @param null $block
1729
     *
1730
     * @return \LesserPhp\NodeEnv
1731
     */
1732
    protected function pushEnv($parent, $block = null)
1733
    {
1734 49
        $e = new NodeEnv();
1735 49
        $e->setParent($parent);
1736 49
        $e->setBlock($block);
1737 49
        $e->setStore([]);
1738
1739 49
        $this->env = $e;
1740
1741 49
        return $e;
1742
    }
1743
1744
    /**
1745
     * pop something off the stack
1746
     *
1747
     * @return \LesserPhp\NodeEnv
1748
     */
1749
    protected function popEnv()
1750
    {
1751 40
        $old = $this->env;
1752 40
        $this->env = $this->env->getParent();
1753
1754 40
        return $old;
1755
    }
1756
1757
    /**
1758
     * set something in the current env
1759
     *
1760
     * @param $name
1761
     * @param $value
1762
     */
1763
    protected function set($name, $value)
1764
    {
1765 27
        $this->env->addStore($name, $value);
1766 27
    }
1767
1768
    /**
1769
     * get the highest occurrence entry for a name
1770
     *
1771
     * @param $name
1772
     *
1773
     * @return array
1774
     * @throws \LesserPhp\Exception\GeneralException
1775
     */
1776
    protected function get($name)
1777
    {
1778 28
        $current = $this->env;
1779
1780
        // track scope to evaluate
1781 28
        $scopeSecondary = [];
1782
1783 28
        $isArguments = $name === $this->vPrefix . 'arguments';
1784 28
        while ($current) {
1785 28
            if ($isArguments && count($current->getArguments()) > 0) {
1786 3
                return ['list', ' ', $current->getArguments()];
1787
            }
1788
1789 28
            if (isset($current->getStore()[$name])) {
1790 27
                return $current->getStore()[$name];
1791
            }
1792
            // has secondary scope?
1793 19
            if (isset($current->storeParent)) {
1794
                $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...
1795
            }
1796
1797 19
            if ($current->getParent() !== null) {
1798 19
                $current = $current->getParent();
1799
            } else {
1800 1
                $current = null;
1801
            }
1802
        }
1803
1804 1
        while (count($scopeSecondary)) {
1805
            // pop one off
1806
            $current = array_shift($scopeSecondary);
1807
            while ($current) {
1808
                if ($isArguments && isset($current->arguments)) {
1809
                    return ['list', ' ', $current->arguments];
1810
                }
1811
1812
                if (isset($current->store[$name])) {
1813
                    return $current->store[$name];
1814
                }
1815
1816
                // has secondary scope?
1817
                if (isset($current->storeParent)) {
1818
                    $scopeSecondary[] = $current->storeParent;
1819
                }
1820
1821
                if (isset($current->parent)) {
1822
                    $current = $current->parent;
1823
                } else {
1824
                    $current = null;
1825
                }
1826
            }
1827
        }
1828
1829 1
        throw new GeneralException("variable $name is undefined");
1830
    }
1831
1832
    /**
1833
     * inject array of unparsed strings into environment as variables
1834
     *
1835
     * @param string[] $args
1836
     *
1837
     * @throws \LesserPhp\Exception\GeneralException
1838
     */
1839
    protected function injectVariables(array $args)
1840
    {
1841 1
        $this->pushEnv($this->env);
1842 1
        $parser = new Parser($this, __METHOD__);
1843 1
        foreach ($args as $name => $strValue) {
1844 1
            if ($name{0} !== '@') {
1845 1
                $name = '@' . $name;
1846
            }
1847 1
            $parser->count = 0;
1848 1
            $parser->buffer = (string) $strValue;
1849 1
            if (!$parser->propertyValue($value)) {
1850
                throw new GeneralException("failed to parse passed in variable $name: $strValue");
1851
            }
1852
1853 1
            $this->set($name, $value);
1854
        }
1855 1
    }
1856
1857
    /**
1858
     * @param string $string
1859
     * @param string $name
1860
     *
1861
     * @return string
1862
     * @throws \LesserPhp\Exception\GeneralException
1863
     */
1864
    public function compile($string, $name = null)
1865
    {
1866 49
        $locale = setlocale(LC_NUMERIC, 0);
1867 49
        setlocale(LC_NUMERIC, 'C');
1868
1869 49
        $this->parser = $this->makeParser($name);
1870 49
        $root = $this->parser->parse($string);
1871
1872 49
        $this->env = null;
1873 49
        $this->scope = null;
1874 49
        $this->allParsedFiles = [];
1875
1876 49
        $this->formatter = $this->newFormatter();
1877
1878 49
        if (!empty($this->registeredVars)) {
1879 1
            $this->injectVariables($this->registeredVars);
1880
        }
1881
1882 49
        $this->sourceParser = $this->parser; // used for error messages
1883 49
        $this->compileBlock($root);
1884
1885 38
        ob_start();
1886 38
        $this->formatter->block($this->scope);
1887 38
        $out = ob_get_clean();
1888 38
        setlocale(LC_NUMERIC, $locale);
1889
1890 38
        return $out;
1891
    }
1892
1893
    /**
1894
     * @param string $fname
1895
     * @param string $outFname
1896
     *
1897
     * @return int|string
1898
     * @throws \LesserPhp\Exception\GeneralException
1899
     */
1900
    public function compileFile($fname, $outFname = null)
1901
    {
1902 1
        if (!is_readable($fname)) {
1903
            throw new GeneralException('load error: failed to find ' . $fname);
1904
        }
1905
1906 1
        $pi = pathinfo($fname);
1907
1908 1
        $oldImport = $this->importDirs;
1909
1910 1
        $this->importDirs[] = $pi['dirname'] . '/';
1911
1912 1
        $this->addParsedFile($fname);
1913
1914 1
        $out = $this->compile(file_get_contents($fname), $fname);
1915
1916 1
        $this->importDirs = $oldImport;
1917
1918 1
        if ($outFname !== null) {
1919
            return file_put_contents($outFname, $out);
1920
        }
1921
1922 1
        return $out;
1923
    }
1924
1925
    /**
1926
     * Based on explicit input/output files does a full change check on cache before compiling.
1927
     *
1928
     * @param string  $in
1929
     * @param string  $out
1930
     * @param boolean $force
1931
     *
1932
     * @return string Compiled CSS results
1933
     * @throws GeneralException
1934
     */
1935
    public function checkedCachedCompile($in, $out, $force = false)
1936
    {
1937 1
        if (!is_file($in) || !is_readable($in)) {
1938
            throw new GeneralException('Invalid or unreadable input file specified.');
1939
        }
1940 1
        if (is_dir($out) || !is_writable(file_exists($out) ? $out : dirname($out))) {
1941
            throw new GeneralException('Invalid or unwritable output file specified.');
1942
        }
1943
1944 1
        $outMeta = $out . '.meta';
1945 1
        $metadata = null;
1946 1
        if (!$force && is_file($outMeta)) {
1947
            $metadata = unserialize(file_get_contents($outMeta));
1948
        }
1949
1950 1
        $output = $this->cachedCompile($metadata ?: $in);
1951
1952 1
        if (!$metadata || $metadata['updated'] != $output['updated']) {
1953 1
            $css = $output['compiled'];
1954 1
            unset($output['compiled']);
1955 1
            file_put_contents($out, $css);
1956 1
            file_put_contents($outMeta, serialize($output));
1957
        } else {
1958
            $css = file_get_contents($out);
1959
        }
1960
1961 1
        return $css;
1962
    }
1963
1964
    /**
1965
     * compile only if changed input has changed or output doesn't exist
1966
     *
1967
     * @param string $in
1968
     * @param string $out
1969
     *
1970
     * @return bool
1971
     * @throws \LesserPhp\Exception\GeneralException
1972
     */
1973
    public function checkedCompile($in, $out)
1974
    {
1975
        if (!is_file($out) || filemtime($in) > filemtime($out)) {
1976
            $this->compileFile($in, $out);
1977
1978
            return true;
1979
        }
1980
1981
        return false;
1982
    }
1983
1984
    /**
1985
     * Execute lessphp on a .less file or a lessphp cache structure
1986
     *
1987
     * The lessphp cache structure contains information about a specific
1988
     * less file having been parsed. It can be used as a hint for future
1989
     * calls to determine whether or not a rebuild is required.
1990
     *
1991
     * The cache structure contains two important keys that may be used
1992
     * externally:
1993
     *
1994
     * compiled: The final compiled CSS
1995
     * updated: The time (in seconds) the CSS was last compiled
1996
     *
1997
     * The cache structure is a plain-ol' PHP associative array and can
1998
     * be serialized and unserialized without a hitch.
1999
     *
2000
     * @param mixed $in    Input
2001
     * @param bool  $force Force rebuild?
2002
     *
2003
     * @return array lessphp cache structure
2004
     * @throws \LesserPhp\Exception\GeneralException
2005
     */
2006
    public function cachedCompile($in, $force = false)
2007
    {
2008
        // assume no root
2009 1
        $root = null;
2010
2011 1
        if (is_string($in)) {
2012 1
            $root = $in;
2013
        } elseif (is_array($in) && isset($in['root'])) {
2014
            if ($force || !isset($in['files'])) {
2015
                // If we are forcing a recompile or if for some reason the
2016
                // structure does not contain any file information we should
2017
                // specify the root to trigger a rebuild.
2018
                $root = $in['root'];
2019
            } elseif (isset($in['files']) && is_array($in['files'])) {
2020
                foreach ($in['files'] as $fname => $ftime) {
2021
                    if (!file_exists($fname) || filemtime($fname) > $ftime) {
2022
                        // One of the files we knew about previously has changed
2023
                        // so we should look at our incoming root again.
2024
                        $root = $in['root'];
2025
                        break;
2026
                    }
2027
                }
2028
            }
2029
        } else {
2030
            // TODO: Throw an exception? We got neither a string nor something
2031
            // that looks like a compatible lessphp cache structure.
2032
            return null;
2033
        }
2034
2035 1
        if ($root !== null) {
2036
            // If we have a root value which means we should rebuild.
2037 1
            $out = [];
2038 1
            $out['root'] = $root;
2039 1
            $out['compiled'] = $this->compileFile($root);
2040 1
            $out['files'] = $this->allParsedFiles;
2041 1
            $out['updated'] = time();
2042
2043 1
            return $out;
2044
        } else {
2045
            // No changes, pass back the structure
2046
            // we were given initially.
2047
            return $in;
2048
        }
2049
    }
2050
2051
    /**
2052
     * parse and compile buffer
2053
     * This is deprecated
2054
     *
2055
     * @param null $str
2056
     * @param null $initialVariables
2057
     *
2058
     * @return int|string
2059
     * @throws \LesserPhp\Exception\GeneralException
2060
     * @deprecated
2061
     */
2062
    public function parse($str = null, $initialVariables = null)
2063
    {
2064 37
        if (is_array($str)) {
2065
            $initialVariables = $str;
2066
            $str = null;
2067
        }
2068
2069 37
        $oldVars = $this->registeredVars;
2070 37
        if ($initialVariables !== null) {
2071 1
            $this->setVariables($initialVariables);
2072
        }
2073
2074 37
        if ($str === null) {
2075
            throw new GeneralException('nothing to parse');
2076
        } else {
2077 37
            $out = $this->compile($str);
2078
        }
2079
2080 37
        $this->registeredVars = $oldVars;
2081
2082 37
        return $out;
2083
    }
2084
2085
    /**
2086
     * @param string $name
2087
     *
2088
     * @return \LesserPhp\Parser
2089
     */
2090
    protected function makeParser($name)
2091
    {
2092 49
        $parser = new Parser($this, $name);
2093 49
        $parser->setWriteComments($this->preserveComments);
2094
2095 49
        return $parser;
2096
    }
2097
2098
    /**
2099
     * @param string $name
2100
     */
2101
    public function setFormatter($name)
2102
    {
2103 1
        $this->formatterName = $name;
2104 1
    }
2105
2106
    /**
2107
     * @return \LesserPhp\Formatter\FormatterInterface
2108
     */
2109
    protected function newFormatter()
2110
    {
2111 49
        $className = 'Lessjs';
2112 49
        if (!empty($this->formatterName)) {
2113 1
            if (!is_string($this->formatterName)) {
2114
                return $this->formatterName;
2115
            }
2116 1
            $className = $this->formatterName;
2117
        }
2118
2119 49
        $className = '\LesserPhp\Formatter\\' . $className;
2120
2121 49
        return new $className;
2122
    }
2123
2124
    /**
2125
     * @param bool $preserve
2126
     */
2127
    public function setPreserveComments($preserve)
2128
    {
2129 1
        $this->preserveComments = $preserve;
2130 1
    }
2131
2132
    /**
2133
     * @param string   $name
2134
     * @param callable $func
2135
     */
2136
    public function registerFunction($name, callable $func)
2137
    {
2138 1
        $this->libFunctions[$name] = $func;
2139 1
    }
2140
2141
    /**
2142
     * @param string $name
2143
     */
2144
    public function unregisterFunction($name)
2145
    {
2146 1
        unset($this->libFunctions[$name]);
2147 1
    }
2148
2149
    /**
2150
     * @param array $variables
2151
     */
2152
    public function setVariables(array $variables)
2153
    {
2154 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...
2155 1
    }
2156
2157
    /**
2158
     * @param $name
2159
     */
2160
    public function unsetVariable($name)
2161
    {
2162
        unset($this->registeredVars[$name]);
2163
    }
2164
2165
    /**
2166
     * @param string[] $dirs
2167
     */
2168
    public function setImportDirs(array $dirs)
2169
    {
2170 38
        $this->importDirs = $dirs;
2171 38
    }
2172
2173
    /**
2174
     * @param string $dir
2175
     */
2176
    public function addImportDir($dir)
2177
    {
2178
        $this->importDirs[] = $dir;
2179
    }
2180
2181
    /**
2182
     * @return string[]
2183
     */
2184
    public function getImportDirs()
2185
    {
2186 4
        return $this->importDirs;
2187
    }
2188
2189
    /**
2190
     * @param string $file
2191
     */
2192
    public function addParsedFile($file)
2193
    {
2194 2
        $this->allParsedFiles[realpath($file)] = filemtime($file);
2195 2
    }
2196
2197
    /**
2198
     * Uses the current value of $this->count to show line and line number
2199
     *
2200
     * @param string $msg
2201
     *
2202
     * @throws GeneralException
2203
     */
2204
    public function throwError($msg = null)
2205
    {
2206
        if ($this->sourceLoc >= 0) {
2207
            $this->sourceParser->throwError($msg, $this->sourceLoc);
2208
        }
2209
        throw new GeneralException($msg);
2210
    }
2211
2212
    /**
2213
     * compile file $in to file $out if $in is newer than $out
2214
     * returns true when it compiles, false otherwise
2215
     *
2216
     * @param                          $in
2217
     * @param                          $out
2218
     * @param \LesserPhp\Compiler|null $less
2219
     *
2220
     * @return bool
2221
     * @throws \LesserPhp\Exception\GeneralException
2222
     */
2223
    public static function ccompile($in, $out, Compiler $less = null)
2224
    {
2225
        if ($less === null) {
2226
            $less = new self;
2227
        }
2228
2229
        return $less->checkedCompile($in, $out);
2230
    }
2231
2232
    /**
2233
     * @param                          $in
2234
     * @param bool                     $force
2235
     * @param \LesserPhp\Compiler|null $less
2236
     *
2237
     * @return array
2238
     * @throws \LesserPhp\Exception\GeneralException
2239
     */
2240
    public static function cexecute($in, $force = false, Compiler $less = null)
2241
    {
2242
        if ($less === null) {
2243
            $less = new self;
2244
        }
2245
2246
        return $less->cachedCompile($in, $force);
2247
    }
2248
2249
    /**
2250
     * prefix of abstract properties
2251
     *
2252
     * @return string
2253
     */
2254
    public function getVPrefix()
2255
    {
2256 49
        return $this->vPrefix;
2257
    }
2258
2259
    /**
2260
     * prefix of abstract blocks
2261
     *
2262
     * @return string
2263
     */
2264
    public function getMPrefix()
2265
    {
2266 46
        return $this->mPrefix;
2267
    }
2268
2269
    /**
2270
     * @return string
2271
     */
2272
    public function getParentSelector()
2273
    {
2274 3
        return $this->parentSelector;
2275
    }
2276
2277
    /**
2278
     * @param int $numberPresicion
2279
     */
2280
    protected function setNumberPrecision($numberPresicion = null)
2281
    {
2282
        $this->numberPrecision = $numberPresicion;
2283
    }
2284
2285
    /**
2286
     * @return \LesserPhp\Library\Coerce
2287
     */
2288
    protected function getCoerce()
2289
    {
2290
        return $this->coerce;
2291
    }
2292
2293
    public function setImportDisabled()
2294
    {
2295 1
        $this->importDisabled = true;
2296 1
    }
2297
2298
    /**
2299
     * @return bool
2300
     */
2301
    public function isImportDisabled()
2302
    {
2303 3
        return $this->importDisabled;
2304
    }
2305
}
2306