Completed
Pull Request — master (#10)
by Michael
02:14
created

Compiler::importCss()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 8
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 8
ccs 1
cts 1
cp 1
rs 9.4285
c 0
b 0
f 0
cc 2
eloc 5
nc 2
nop 1
crap 2
1
<?php
2
3
namespace LesserPhp;
4
5
/**
6
 * lesserphp
7
 * https://www.maswaba.de/lesserphp
8
 *
9
 * LESS CSS compiler, adapted from http://lesscss.org
10
 *
11
 * Copyright 2013, Leaf Corcoran <[email protected]>
12
 * Copyright 2016, Marcus Schwarz <[email protected]>
13
 * Licensed under MIT or GPLv3, see LICENSE
14
 * @package LesserPhp
15
 */
16
use LesserPhp\Color\Converter;
17
use LesserPhp\Exception\GeneralException;
18
use LesserPhp\Library\Assertions;
19
use LesserPhp\Library\Coerce;
20
use LesserPhp\Library\Functions;
21
22
/**
23
 * The LESS compiler and parser.
24
 *
25
 * Converting LESS to CSS is a three stage process. The incoming file is parsed
26
 * by `lessc_parser` into a syntax tree, then it is compiled into another tree
27
 * representing the CSS structure by `lessc`. The CSS tree is fed into a
28
 * formatter, like `lessc_formatter` which then outputs CSS as a string.
29
 *
30
 * During the first compile, all values are *reduced*, which means that their
31
 * types are brought to the lowest form before being dump as strings. This
32
 * handles math equations, variable dereferences, and the like.
33
 *
34
 * The `parse` function of `lessc` is the entry point.
35
 *
36
 * In summary:
37
 *
38
 * The `lessc` class creates an instance of the parser, feeds it LESS code,
39
 * then transforms the resulting tree to a CSS tree. This class also holds the
40
 * evaluation context, such as all available mixins and variables at any given
41
 * time.
42
 *
43
 * The `lessc_parser` class is only concerned with parsing its input.
44
 *
45
 * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
46
 * handling things like indentation.
47
 */
48
class Compiler
49
{
50
    const VERSION = 'v0.5.1';
51
52
    public static $TRUE = ['keyword', 'true'];
53
    public static $FALSE = ['keyword', 'false'];
54
55
    /**
56
     * @var callable[]
57
     */
58
    private $libFunctions = [];
59
60
    /**
61
     * @var string[]
62
     */
63
    private $registeredVars = [];
64
65
    /**
66
     * @var bool
67
     */
68
    protected $preserveComments = false;
69
70
    /**
71
     * @var string $vPrefix prefix of abstract properties
72
     */
73
    private $vPrefix = '@';
74
75
    /**
76
     * @var string $mPrefix prefix of abstract blocks
77
     */
78
    private $mPrefix = '$';
79
80
    /**
81
     * @var string
82
     */
83
    private $parentSelector = '&';
84
85
    /**
86
     * @var bool $importDisabled disable @import
87
     */
88
    private $importDisabled = false;
89
90
    /**
91
     * @var string[]
92
     */
93
    private $importDirs = [];
94
95
    /**
96
     * @var int
97
     */
98
    private $numberPrecision;
99
100
    /**
101
     * @var string[]
102
     */
103
    private $allParsedFiles = [];
104
105
    /**
106
     * set to the parser that generated the current line when compiling
107
     * so we know how to create error messages
108
     * @var \LesserPhp\Parser
109
     */
110
    private $sourceParser;
111
112
    /**
113
     * @var integer $sourceLoc Lines of Code
114
     */
115
    private $sourceLoc;
116
117
    /**
118
     * @var int $nextImportId uniquely identify imports
119
     */
120
    private static $nextImportId = 0;
121
122
    /**
123
     * @var Parser
124
     */
125
    private $parser;
126
127
    /**
128
     * @var \LesserPhp\Formatter\FormatterInterface
129
     */
130
    private $formatter;
131
132
    /**
133
     * @var \LesserPhp\NodeEnv What's the meaning of "env" in this context?
134
     */
135
    private $env;
136
137
    /**
138
     * @var \LesserPhp\Library\Coerce
139
     */
140
    private $coerce;
141
142
    /**
143
     * @var \LesserPhp\Library\Assertions
144
     */
145
    private $assertions;
146
147
    /**
148
     * @var \LesserPhp\Library\Functions
149
     */
150
    private $functions;
151
152
    /**
153
     * @var mixed what's this exactly?
154
     */
155
    private $scope;
156
157
    /**
158
     * @var string
159
     */
160
    private $formatterName;
161
162
    /**
163
     * @var \LesserPhp\Color\Converter
164
     */
165
    private $converter;
166
167
    /**
168
     * @var bool Allow importing of *.css files
169
     * @see tryImport()
170
     * @see importCss()
171
     */
172 64
    private $import_css = false;
173
174 64
    /**
175 64
     * Constructor.
176 64
     *
177 64
     * Hardwires dependencies for now
178 64
     */
179
    public function __construct()
180
    {
181
        $this->coerce = new Coerce();
182
        $this->assertions = new Assertions($this->coerce);
183
        $this->converter = new Converter();
184
        $this->functions = new Functions($this->assertions, $this->coerce, $this, $this->converter);
185
    }
186 48
187
    /**
188 48
     * @param array $items
189 48
     * @param       $delim
190
     *
191 27
     * @return array
192
     */
193
    public static function compressList(array $items, $delim)
194
    {
195
        if (!isset($items[1]) && isset($items[0])) {
196
            return $items[0];
197
        } else {
198
            return ['list', $delim, $items];
199
        }
200 36
    }
201
202 36
    /**
203
     * @param string $what
204
     *
205
     * @return string
206
     */
207
    public static function pregQuote($what)
208
    {
209
        return preg_quote($what, '/');
210
    }
211
212
    /**
213 3
     * @param array $importPath
214
     * @param       $parentBlock
215 3
     * @param       $out
216 2
     *
217
     * @return array|false
218
     * @throws \LesserPhp\Exception\GeneralException
219 3
     */
220 3
    protected function tryImport(array $importPath, $parentBlock, $out)
221 2
    {
222
        if ($importPath[0] === 'function' && $importPath[1] === 'url') {
223
            $importPath = $this->flattenList($importPath[2]);
224 3
        }
225
226
        $str = $this->coerce->coerceString($importPath);
227 3
        if ($str === null) {
228
            return false;
229
        }
230
231 3
        $url = $this->compileValue($this->functions->e($str));
232
233 3
        $pi = pathinfo($url);
234 2
235
        // don't import if it ends in css
236
        if ($pi['extension'] == 'css' && !$this->import_css) {
237 3
            return false;
238 1
        }
239
240
        $realPath = $this->functions->findImport($url);
241 2
242 2
        if ($realPath === null) {
243
            return false;
244
        }
245 2
246 2
        if ($this->isImportDisabled()) {
247 2
            return [false, '/* import disabled */'];
248
        }
249
250 2
        if (isset($this->allParsedFiles[realpath($realPath)])) {
251 2
            return [false, null];
252 2
        }
253
254
        $this->addParsedFile($realPath);
255
        $parser = $this->makeParser($realPath);
256
        $root = $parser->parse(file_get_contents($realPath));
257
258
        // set the parents of all the block props
259 2
        foreach ($root->props as $prop) {
260 2
            if ($prop[0] === 'block') {
261 2
                $prop[1]->parent = $parentBlock;
262 2
            }
263
        }
264
265
        // copy mixins into scope, set their parents
266 2
        // bring blocks from import into current block
267
        // TODO: need to mark the source parser	these came from this file
268
        foreach ($root->children as $childName => $child) {
269
            if (isset($parentBlock->children[$childName])) {
270 2
                $parentBlock->children[$childName] = array_merge(
271 2
                    $parentBlock->children[$childName],
272
                    $child
273 2
                );
274 2
            } else {
275
                $parentBlock->children[$childName] = $child;
276 2
            }
277
        }
278
279
        $dir = $pi["dirname"];
280
281
        list($top, $bottom) = $this->sortProps($root->props, true);
282
283
        if ($pi['extension'] != 'css') {
284
            $this->compileImportedProps($top, $parentBlock, $out, $dir);
285 2
        }
286
287 2
        return [true, $bottom, $parser, $dir];
288
    }
289 2
290
    /**
291 2
     * @param array  $props
292
     * @param        $block
293 2
     * @param        $out
294 2
     * @param string $importDir
295
     */
296
    protected function compileImportedProps(array $props, $block, $out, $importDir)
297 2
    {
298 2
        $oldSourceParser = $this->sourceParser;
299 2
300
        $oldImport = $this->importDirs;
301
302
        array_unshift($this->importDirs, $importDir);
303
304
        foreach ($props as $prop) {
305
            $this->compileProp($prop, $block, $out);
306
        }
307
308
        $this->importDirs = $oldImport;
309
        $this->sourceParser = $oldSourceParser;
310
    }
311
312
    /**
313
     * Recursively compiles a block.
314
     *
315
     * A block is analogous to a CSS block in most cases. A single LESS document
316
     * is encapsulated in a block when parsed, but it does not have parent tags
317
     * so all of it's children appear on the root level when compiled.
318
     *
319
     * Blocks are made up of props and children.
320
     *
321
     * Props are property instructions, array tuples which describe an action
322
     * to be taken, eg. write a property, set a variable, mixin a block.
323
     *
324
     * The children of a block are just all the blocks that are defined within.
325 49
     * This is used to look up mixins when performing a mixin.
326
     *
327 49
     * Compiling the block involves pushing a fresh environment on the stack,
328 49
     * and iterating through the props, compiling each one.
329 49
     *
330 38
     * See lessc::compileProp()
331 46
     *
332 46
     * @param Block $block
333 35
     *
334 6
     * @throws \LesserPhp\Exception\GeneralException
335 3
     */
336 3
    protected function compileBlock(Block $block)
337 4
    {
338 4
        switch ($block->type) {
339 4
            case "root":
340 2
                $this->compileRoot($block);
341
                break;
342
            case null:
0 ignored issues
show
Bug introduced by
It seems like you are loosely comparing $block->type of type string|null against null; this is ambiguous if the string can be empty. Consider using a strict comparison === instead.
Loading history...
343 4
                $this->compileCSSBlock($block);
344 4
                break;
345
            case "media":
346
                $this->compileMedia($block);
0 ignored issues
show
Compatibility introduced by
$block of type object<LesserPhp\Block> is not a sub-type of object<LesserPhp\Block\Media>. It seems like you assume a child class of the class LesserPhp\Block to be always present.

This check looks for parameters that are defined as one type in their type hint or doc comment but seem to be used as a narrower type, i.e an implementation of an interface or a subclass.

Consider changing the type of the parameter or doing an instanceof check before assuming your parameter is of the expected type.

Loading history...
347
                break;
348 38
            case "directive":
349
                $name = "@" . $block->name;
0 ignored issues
show
Bug introduced by
The property name does not seem to exist in LesserPhp\Block.

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...
350
                if (!empty($block->value)) {
351
                    $name .= " " . $this->compileValue($this->reduce($block->value));
0 ignored issues
show
Bug introduced by
The property value does not seem to exist in LesserPhp\Block.

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...
352
                }
353
354
                $this->compileNestedBlock($block, [$name]);
355 46
                break;
356
            default:
357 46
                $block->parser->throwError("unknown block type: $block->type\n", $block->count);
358
        }
359 46
    }
360 46
361 46
    /**
362
     * @param Block $block
363 46
     *
364 46
     * @throws \LesserPhp\Exception\GeneralException
365
     */
366 35
    protected function compileCSSBlock(Block $block)
367 35
    {
368 35
        $env = $this->pushEnv($this->env);
369
370
        $selectors = $this->compileSelectors($block->tags);
0 ignored issues
show
Bug introduced by
It seems like $block->tags can also be of type null; however, LesserPhp\Compiler::compileSelectors() does only seem to accept array, maybe add an additional type check?

If a method or function can return multiple different values and unless you are sure that you only can receive a single value in this context, we recommend to add an additional type check:

/**
 * @return array|string
 */
function returnsDifferentValues($x) {
    if ($x) {
        return 'foo';
    }

    return array();
}

$x = returnsDifferentValues($y);
if (is_array($x)) {
    // $x is an array.
}

If this a common case that PHP Analyzer should handle natively, please let us know by opening an issue.

Loading history...
371
        $env->setSelectors($this->multiplySelectors($selectors));
372
        $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...
373 3
374
        $this->scope->children[] = $out;
375 3
        $this->compileProps($block, $out);
376 3
377
        $block->scope = $env; // mixins carry scope with them!
0 ignored issues
show
Bug introduced by
The property scope does not seem to exist in LesserPhp\Block.

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...
378 3
        $this->popEnv();
379
    }
380 3
381 3
    /**
382
     * @param Block\Media $media
383 3
     */
384
    protected function compileMedia(Block\Media $media)
385 3
    {
386 3
        $env = $this->pushEnv($this->env, $media);
387 3
        $parentScope = $this->mediaParent($this->scope);
388 3
389 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...
390 3
391 3
        $this->scope = $this->makeOutputBlock($media->type, [$query]);
392
        $parentScope->children[] = $this->scope;
393
394
        $this->compileProps($media, $this->scope);
395 3
396 3
        if (count($this->scope->lines) > 0) {
397 3
            $orphanSelelectors = $this->findClosestSelectors();
398
            if ($orphanSelelectors !== null) {
399
                $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
400
                $orphan->lines = $this->scope->lines;
401
                array_unshift($this->scope->children, $orphan);
402
                $this->scope->lines = [];
403
            }
404 3
        }
405
406 3
        $this->scope = $this->scope->parent;
407 1
        $this->popEnv();
408 1
    }
409
410 1
    /**
411
     * @param $scope
412
     *
413 3
     * @return mixed
414
     */
415
    protected function mediaParent($scope)
416
    {
417
        while (!empty($scope->parent)) {
418
            if (!empty($scope->type) && $scope->type !== "media") {
419
                break;
420 4
            }
421
            $scope = $scope->parent;
422 4
        }
423 4
424 4
        return $scope;
425
    }
426 4
427
    /**
428 4
     * @param Block    $block
429 4
     * @param string[] $selectors
430 4
     */
431
    protected function compileNestedBlock(Block $block, array $selectors)
432
    {
433
        $this->pushEnv($this->env, $block);
434
        $this->scope = $this->makeOutputBlock($block->type, $selectors);
435 49
        $this->scope->parent->children[] = $this->scope;
436
437 49
        $this->compileProps($block, $this->scope);
438 49
439 49
        $this->scope = $this->scope->parent;
440 38
        $this->popEnv();
441 38
    }
442
443
    /**
444
     * @param Block $root
445
     */
446
    protected function compileRoot(Block $root)
447
    {
448
        $this->pushEnv($this->env);
449 49
        $this->scope = $this->makeOutputBlock($root->type);
450
        $this->compileProps($root, $this->scope);
451 49
        $this->popEnv();
452 49
    }
453
454 38
    /**
455 38
     * @param Block $block
456
     * @param \stdClass $out
457
     *
458
     * @throws \LesserPhp\Exception\GeneralException
459
     */
460
    protected function compileProps(Block $block, $out)
461
    {
462
        foreach ($this->sortProps($block->props) as $prop) {
463
            $this->compileProp($prop, $block, $out);
464
        }
465
        $out->lines = $this->deduplicate($out->lines);
466 38
    }
467
468 38
    /**
469 38
     * Deduplicate lines in a block. Comments are not deduplicated. If a
470
     * duplicate rule is detected, the comments immediately preceding each
471 38
     * occurence are consolidated.
472 38
     *
473 2
     * @param array $lines
474 2
     *
475
     * @return array
476 37
     */
477 37
    protected function deduplicate(array $lines)
478
    {
479 37
        $unique = [];
480 37
        $comments = [];
481
482
        foreach ($lines as $line) {
483 38
            if (strpos($line, '/*') === 0) {
484
                $comments[] = $line;
485
                continue;
486
            }
487
            if (!in_array($line, $unique)) {
488
                $unique[] = $line;
489
            }
490
            array_splice($unique, array_search($line, $unique), 0, $comments);
491
            $comments = [];
492 49
        }
493
494 49
        return array_merge($unique, $comments);
495 49
    }
496 49
497 49
    /**
498
     * @param array $props
499 49
     * @param bool  $split
500 49
     *
501 49
     * @return array
502 1
     */
503 1
    protected function sortProps(array $props, $split = false)
504 49
    {
505 43
        $vars = [];
506 43
        $imports = [];
507 23
        $other = [];
508
        $stack = [];
509 43
510
        foreach ($props as $prop) {
511 43
            switch ($prop[0]) {
512 43
                case "comment":
513 47
                    $stack[] = $prop;
514 3
                    break;
515 3
                case "assign":
516 3
                    $stack[] = $prop;
517 3
                    if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
518 3
                        $vars = array_merge($vars, $stack);
519 3
                    } else {
520 3
                        $other = array_merge($other, $stack);
521
                    }
522 46
                    $stack = [];
523 46
                    break;
524 46
                case "import":
525 49
                    $id = self::$nextImportId++;
526
                    $prop[] = $id;
527
                    $stack[] = $prop;
528 49
                    $imports = array_merge($imports, $stack);
529
                    $other[] = ["import_mixin", $id];
530 49
                    $stack = [];
531 2
                    break;
532
                default:
533 49
                    $stack[] = $prop;
534
                    $other = array_merge($other, $stack);
535
                    $stack = [];
536
                    break;
537
            }
538
        }
539
        $other = array_merge($other, $stack);
540
541
        if ($split) {
542 3
            return [array_merge($imports, $vars), $other];
543
        } else {
544 3
            return array_merge($imports, $vars, $other);
545 3
        }
546 3
    }
547 3
548 3
    /**
549 3
     * @param array $queries
550 3
     *
551 3
     * @return string
552 1
     */
553 1
    protected function compileMediaQuery(array $queries)
554 1
    {
555 1
        $compiledQueries = [];
556
        foreach ($queries as $query) {
557 1
            $parts = [];
558
            foreach ($query as $q) {
559 1
                switch ($q[0]) {
560 1
                    case "mediaType":
561 1
                        $parts[] = implode(" ", array_slice($q, 1));
562 3
                        break;
563
                    case "mediaExp":
564
                        if (isset($q[2])) {
565
                            $parts[] = "($q[1]: " .
566 3
                                $this->compileValue($this->reduce($q[2])) . ")";
567 3
                        } else {
568
                            $parts[] = "($q[1])";
569
                        }
570
                        break;
571 3
                    case "variable":
572 3
                        $parts[] = $this->compileValue($this->reduce($q));
573
                        break;
574 3
                }
575
            }
576
577 3
            if (count($parts) > 0) {
578
                $compiledQueries[] = implode(" and ", $parts);
579
            }
580
        }
581
582
        $out = "@media";
583
        if (!empty($parts)) {
584
            $out .= " " .
585
                implode($this->formatter->getSelectorSeparator(), $compiledQueries);
586 3
        }
587
588 3
        return $out;
589 3
    }
590
591 3
    /**
592
     * @param \LesserPhp\NodeEnv $env
593
     * @param array              $childQueries
594
     *
595 3
     * @return array
596 3
     */
597
    protected function multiplyMedia(NodeEnv $env = null, array $childQueries = null)
598
    {
599 3
        if ($env === null ||
600 3
            (!empty($env->getBlock()->type) && $env->getBlock()->type !== 'media')
601 3
        ) {
602 3
            return $childQueries;
603
        }
604 1
605 1
        // plain old block, skip
606 1
        if (empty($env->getBlock()->type)) {
607
            return $this->multiplyMedia($env->getParent(), $childQueries);
608
        }
609
610
        $out = [];
611 3
        $queries = $env->getBlock()->queries;
612
        if ($childQueries === null) {
613
            $out = $queries;
614
        } else {
615
            foreach ($queries as $parent) {
616
                foreach ($childQueries as $child) {
617
                    $out[] = array_merge($parent, $child);
618
                }
619
            }
620 46
        }
621
622 46
        return $this->multiplyMedia($env->getParent(), $out);
623 46
    }
624 46
625 46
    /**
626 46
     * @param $tag
627
     * @param $replace
628 46
     *
629
     * @return int
630 46
     */
631
    protected function expandParentSelectors(&$tag, $replace)
632
    {
633
        $parts = explode("$&$", $tag);
634
        $count = 0;
635
        foreach ($parts as &$part) {
636 46
            $part = str_replace($this->parentSelector, $replace, $part, $c);
637
            $count += $c;
638 46
        }
639 46
        $tag = implode($this->parentSelector, $parts);
640 46
641 46
        return $count;
642 13
    }
643 13
644
    /**
645 46
     * @return array|null
646
     */
647
    protected function findClosestSelectors()
648 46
    {
649
        $env = $this->env;
650
        $selectors = null;
651
        while ($env !== null) {
652
            if ($env->getSelectors() !== null) {
653
                $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...
654
                break;
655
            }
656
            $env = $env->getParent();
657
        }
658
659 46
        return $selectors;
660
    }
661
662
663 46
    /**
664 46
     *  multiply $selectors against the nearest selectors in env
665
     *
666 46
     * @param array $selectors
667 46
     *
668
     * @return array
669
     */
670 46
    protected function multiplySelectors(array $selectors)
671
    {
672
        // find parent selectors
673 13
674 13
        $parentSelectors = $this->findClosestSelectors();
675 13
        if ($parentSelectors === null) {
676 13
            // kill parent reference in top level selector
677
            foreach ($selectors as &$s) {
678
                $this->expandParentSelectors($s, "");
679 13
            }
680 4
681
            return $selectors;
682 13
        }
683
684
        $out = [];
685
        foreach ($parentSelectors as $parent) {
686
            foreach ($selectors as $child) {
687 13
                $count = $this->expandParentSelectors($child, $parent);
688
689
                // don't prepend the parent tag if & was used
690
                if ($count > 0) {
691
                    $out[] = trim($child);
692
                } else {
693
                    $out[] = trim($parent . ' ' . $child);
694
                }
695
            }
696
        }
697
698 46
        return $out;
699
    }
700 46
701
    /**
702 46
     * reduces selector expressions
703 46
     *
704 4
     * @param array $selectors
705 4
     *
706
     * @return array
707 46
     * @throws \LesserPhp\Exception\GeneralException
708
     */
709
    protected function compileSelectors(array $selectors)
710
    {
711 46
        $out = [];
712
713
        foreach ($selectors as $s) {
714
            if (is_array($s)) {
715
                list(, $value) = $s;
716
                $out[] = trim($this->compileValue($this->reduce($value)));
717
            } else {
718
                $out[] = $s;
719
            }
720 4
        }
721
722 4
        return $out;
723
    }
724
725
    /**
726
     * @param $left
727
     * @param $right
728
     *
729
     * @return bool
730
     */
731
    protected function equals($left, $right)
732 21
    {
733
        return $left == $right;
734
    }
735
736 21
    /**
737 5
     * @param $block
738 5
     * @param $orderedArgs
739 5
     * @param $keywordArgs
740 5
     *
741 5
     * @return bool
742
     */
743 5
    protected function patternMatch($block, $orderedArgs, $keywordArgs)
744 5
    {
745 1
        // match the guards if it has them
746 1
        // any one of the groups must have all its guards pass for a match
747
        if (!empty($block->guards)) {
748
            $groupPassed = false;
749 5
            foreach ($block->guards as $guardGroup) {
750 5
                foreach ($guardGroup as $guard) {
751 1
                    $this->pushEnv($this->env);
752
                    $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
753
754 5
                    $negate = false;
755
                    if ($guard[0] === "negate") {
756 5
                        $guard = $guard[1];
757 3
                        $negate = true;
758
                    }
759 5
760 5
                    $passed = $this->reduce($guard) == self::$TRUE;
761
                    if ($negate) {
762
                        $passed = !$passed;
763
                    }
764 5
765 5
                    $this->popEnv();
766
767
                    if ($passed) {
768
                        $groupPassed = true;
769 5
                    } else {
770 5
                        $groupPassed = false;
771
                        break;
772
                    }
773
                }
774 19
775 12
                if ($groupPassed) {
776
                    break;
777
                }
778 14
            }
779 14
780 2
            if (!$groupPassed) {
781 2
                return false;
782 2
            }
783 2
        }
784
785
        if (empty($block->args)) {
786 2
            return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
787
        }
788
789
        $remainingArgs = $block->args;
790 14
        if ($keywordArgs) {
791
            $remainingArgs = [];
792 14
            foreach ($block->args as $arg) {
793 14
                if ($arg[0] === "arg" && isset($keywordArgs[$arg[1]])) {
794 14
                    continue;
795 3
                }
796 2
797
                $remainingArgs[] = $arg;
798 3
            }
799 14
        }
800
801 14
        $i = -1; // no args
802 3
        // try to match by arity or by argument literal
803
        foreach ($remainingArgs as $i => $arg) {
804 14
            switch ($arg[0]) {
805 2
                case "lit":
806 2
                    if (empty($orderedArgs[$i]) || !$this->equals($arg[1], $orderedArgs[$i])) {
807 14
                        return false;
808
                    }
809
                    break;
810
                case "arg":
811 13
                    // no arg and no default value
812 2
                    if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
813
                        return false;
814 13
                    }
815
                    break;
816
                case "rest":
817 13
                    $i--; // rest can be empty
818
                    break 2;
819
            }
820
        }
821
822
        if ($block->isVararg) {
823
            return true; // not having enough is handled above
824
        } else {
825
            $numMatched = $i + 1;
826
827
            // greater than because default values always match
828
            return $numMatched >= count($orderedArgs);
829 21
        }
830
    }
831 21
832 21
    /**
833
     * @param array $blocks
834 21
     * @param       $orderedArgs
835 1
     * @param       $keywordArgs
836
     * @param array $skip
837
     *
838 21
     * @return array|null
839 21
     */
840
    protected function patternMatchAll(array $blocks, $orderedArgs, $keywordArgs, array $skip = [])
841
    {
842
        $matches = null;
843 21
        foreach ($blocks as $block) {
844
            // skip seen blocks that don't have arguments
845
            if (isset($skip[$block->id]) && !isset($block->args)) {
846
                continue;
847
            }
848
849
            if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
850
                $matches[] = $block;
851
            }
852
        }
853
854
        return $matches;
855
    }
856
857 22
    /**
858
     * attempt to find blocks matched by path and args
859 22
     *
860 5
     * @param       $searchIn
861
     * @param array $path
862 22
     * @param       $orderedArgs
863 1
     * @param       $keywordArgs
864
     * @param array $seen
865 22
     *
866
     * @return array|null
867 22
     */
868
    protected function findBlocks($searchIn, array $path, $orderedArgs, $keywordArgs, array $seen = [])
869 22
    {
870 21
        if ($searchIn === null) {
871 21
            return null;
872 21
        }
873 21
        if (isset($seen[$searchIn->id])) {
874
            return null;
875
        }
876 21
        $seen[$searchIn->id] = true;
877
878
        $name = $path[0];
879 3
880 3
        if (isset($searchIn->children[$name])) {
881 3
            $blocks = $searchIn->children[$name];
882
            if (count($path) === 1) {
883 3
                $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
884
                if (!empty($matches)) {
885
                    // This will return all blocks that match in the closest
886
                    // scope that has any matching block, like lessjs
887
                    return $matches;
888
                }
889 3
            } else {
890 3
                $matches = [];
891 3
                foreach ($blocks as $subBlock) {
892
                    $subMatches = $this->findBlocks(
893
                        $subBlock,
894
                        array_slice($path, 1),
895
                        $orderedArgs,
896 3
                        $keywordArgs,
897
                        $seen
898
                    );
899 22
900
                    if ($subMatches !== null) {
901
                        foreach ($subMatches as $sm) {
902
                            $matches[] = $sm;
903 22
                        }
904
                    }
905
                }
906
907
                return count($matches) > 0 ? $matches : null;
908
            }
909
        }
910
        if ($searchIn->parent === $searchIn) {
911
            return null;
912
        }
913
914
        return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
915
    }
916 16
917
    /**
918 16
     * sets all argument names in $args to either the default value
919
     * or the one passed in through $values
920 16
     *
921 16
     * @param array $args
922 14
     * @param       $orderedValues
923 14
     * @param       $keywordValues
924
     *
925 2
     * @throws \LesserPhp\Exception\GeneralException
926 14
     */
927
    protected function zipSetArgs(array $args, $orderedValues, $keywordValues)
928 13
    {
929 13
        $assignedValues = [];
930 6
931
        $i = 0;
932 6
        foreach ($args as $a) {
933
            if ($a[0] === "arg") {
934
                if (isset($keywordValues[$a[1]])) {
935
                    // has keyword arg
936
                    $value = $keywordValues[$a[1]];
937 14
                } elseif (isset($orderedValues[$i])) {
938 14
                    // has ordered arg
939 14
                    $value = $orderedValues[$i];
940
                    $i++;
941
                } elseif (isset($a[2])) {
942 14
                    // has default value
943
                    $value = $a[2];
944
                } else {
945
                    throw new GeneralException('Failed to assign arg ' . $a[1]);
946
                }
947 16
948 16
                $value = $this->reduce($value);
949 2
                $this->set($a[1], $value);
950 2
                $assignedValues[] = $value;
951
            } else {
952
                // a lit
953
                $i++;
954 16
            }
955 16
        }
956
957
        // check for a rest
958
        $last = end($args);
959
        if ($last[0] === "rest") {
960
            $rest = array_slice($orderedValues, count($args) - 1);
961
            $this->set($last[1], $this->reduce(["list", " ", $rest]));
962
        }
963
964
        // wow is this the only true use of PHP's + operator for arrays?
965
        $this->env->setArguments($assignedValues + $orderedValues);
966 49
    }
967
968
    /**
969 49
     * compile a prop and update $lines or $blocks appropriately
970
     *
971 49
     * @param $prop
972 49
     * @param Block $block
973 43
     * @param $out
974 43
     *
975 23
     * @throws \LesserPhp\Exception\GeneralException
976
     */
977 43
    protected function compileProp($prop, Block $block, $out)
978
    {
979 43
        // set error position context
980
        $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
981
982 37
        switch ($prop[0]) {
983 47
            case 'assign':
984 46
                list(, $name, $value) = $prop;
985 46
                if ($name[0] == $this->vPrefix) {
986 35
                    $this->set($name, $value);
987 25
                } else {
988 25
                    $out->lines[] = $this->formatter->property(
989 22
                        $name,
990
                        $this->compileValue($this->reduce($value))
991 22
                    );
992 22
                }
993 22
                break;
994 15
            case 'block':
995 15
                list(, $child) = $prop;
996 4
                $this->compileBlock($child);
997 3
                break;
998
            case 'ruleset':
999 2
            case 'mixin':
1000
                list(, $path, $args, $suffix) = $prop;
1001 4
1002
                $orderedArgs = [];
1003 15
                $keywordArgs = [];
1004 15
                foreach ((array)$args as $arg) {
1005 15
                    switch ($arg[0]) {
1006
                        case "arg":
1007 15
                            if (!isset($arg[2])) {
1008
                                $orderedArgs[] = $this->reduce(["variable", $arg[1]]);
1009
                            } else {
1010
                                $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
1011 22
                            }
1012
                            break;
1013 22
1014 5
                        case "lit":
1015
                            $orderedArgs[] = $this->reduce($arg[1]);
1016
                            break;
1017 17
                        default:
1018
                            throw new GeneralException("Unknown arg type: " . $arg[0]);
1019 8
                    }
1020
                }
1021
1022 17
                $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
1023 17
1024
                if ($mixins === null) {
1025
                    $block->parser->throwError("{$prop[1][0]} is undefined", $block->count);
1026
                }
1027 17
1028 17
                if (strpos($prop[1][0], "$") === 0) {
1029 2
                    //Use Ruleset Logic - Only last element
1030 2
                    $mixins = [array_pop($mixins)];
1031 2
                }
1032
1033
                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...
1034 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...
1035 17
                        continue;
1036 14
                    }
1037 14
1038 14
                    $haveScope = false;
1039
                    if (isset($mixin->parent->scope)) {
1040
                        $haveScope = true;
1041 17
                        $mixinParentEnv = $this->pushEnv($this->env);
1042 17
                        $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...
1043 17
                    }
1044
1045
                    $haveArgs = false;
1046 17
                    if (isset($mixin->args)) {
1047 17
                        $haveArgs = true;
1048 17
                        $this->pushEnv($this->env);
1049 17
                        $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
1050 17
                    }
1051
1052 1
                    $oldParent = $mixin->parent;
1053 1
                    if ($mixin != $block) {
1054 1
                        $mixin->parent = $block;
1055 1
                    }
1056
1057
                    foreach ($this->sortProps($mixin->props) as $subProp) {
1058
                        if ($suffix !== null &&
1059 17
                            $subProp[0] === "assign" &&
1060
                            is_string($subProp[1]) &&
1061
                            $subProp[1]{0} != $this->vPrefix
1062 17
                        ) {
1063
                            $subProp[2] = [
1064 17
                                'list',
1065 14
                                ' ',
1066
                                [$subProp[2], ['keyword', $suffix]],
1067 17
                            ];
1068 17
                        }
1069
1070
                        $this->compileProp($subProp, $mixin, $out);
1071
                    }
1072 17
1073 5
                    $mixin->parent = $oldParent;
1074
1075
                    if ($haveArgs) {
1076 5
                        $this->popEnv();
1077 1
                    }
1078 1
                    if ($haveScope) {
1079 1
                        $this->popEnv();
1080 4
                    }
1081 1
                }
1082 1
1083 3
                break;
1084 3
            case 'raw':
1085 3
                $out->lines[] = $prop[1];
1086
                break;
1087 3
            case "directive":
1088
                list(, $name, $value) = $prop;
1089 3
                $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)) . ';';
1090 2
                break;
1091 3
            case "comment":
1092
                $out->lines[] = $prop[1];
1093 3
                break;
1094 3
            case "import":
1095 3
                list(, $importPath, $importId) = $prop;
1096 3
                $importPath = $this->reduce($importPath);
1097 3
1098 3
                $result = $this->tryImport($importPath, $block, $out);
1099 3
1100
                $this->env->addImports($importId, $result === false ?
1101
                    [false, "@import " . $this->compileValue($importPath) . ";"] :
1102 2
                    $result);
1103 2
1104
                break;
1105
            case "import_mixin":
1106 3
                list(, $importId) = $prop;
1107
                $import = $this->env->getImports($importId);
1108
                if ($import[0] === false) {
1109
                    if (isset($import[1])) {
1110 38
                        $out->lines[] = $import[1];
1111
                    }
1112
                } else {
1113
                    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...
1114
                    $this->compileImportedProps($bottom, $block, $out, $importDir);
1115
                }
1116
1117
                break;
1118
            default:
1119
                $block->parser->throwError("unknown op: {$prop[0]}\n", $block->count);
1120
        }
1121
    }
1122
1123
1124
    /**
1125
     * Compiles a primitive value into a CSS property value.
1126
     *
1127
     * Values in lessphp are typed by being wrapped in arrays, their format is
1128
     * typically:
1129
     *
1130 53
     *     array(type, contents [, additional_contents]*)
1131
     *
1132
     * The input is expected to be reduced. This function will not work on
1133 53
     * things like expressions and variables.
1134
     *
1135
     * @param array $value
1136
     * @param array $options
1137 53
     *
1138 53
     * @return string
1139 53
     * @throws GeneralException
1140
     */
1141
    public function compileValue(array $value, array $options = [])
1142 53
    {
1143
        try {
1144 52
            if (!isset($value[0])) {
1145 1
                throw new GeneralException('Missing value type');
1146 1
            }
1147
1148
            $options = array_replace([
1149
                'numberPrecision' => $this->numberPrecision,
1150
                'compressColors'  => ($this->formatter ? $this->formatter->getCompressColors() : false),
1151
            ], $options);
1152
1153
            $valueClass = \LesserPhp\Compiler\Value\AbstractValue::factory($this, $this->coerce, $options, $value);
1154
1155
            return $valueClass->getCompiled();
1156
        } catch (\UnexpectedValueException $e) {
1157
            throw new GeneralException($e->getMessage());
1158 2
        }
1159
    }
1160 2
1161 1
    /**
1162
     * Helper function to get arguments for color manipulation functions.
1163 2
     * takes a list that contains a color like thing and a percentage
1164 2
     *
1165 2
     * @param array $args
1166
     *
1167 2
     * @return array
1168
     */
1169
    public function colorArgs(array $args)
1170
    {
1171
        if ($args[0] !== 'list' || count($args[2]) < 2) {
1172
            return [['color', 0, 0, 0], 0];
1173
        }
1174
        list($color, $delta) = $args[2];
1175
        $color = $this->assertions->assertColor($color);
1176
        $delta = (float) $delta[1];
1177
1178 24
        return [$color, $delta];
1179
    }
1180 24
1181 24
    /**
1182 6
     * Convert the rgb, rgba, hsl color literals of function type
1183
     * as returned by the parser into values of color type.
1184
     *
1185 24
     * @param array $func
1186
     *
1187 24
     * @return bool|mixed
1188 1
     */
1189 1
    protected function funcToColor(array $func)
1190 1
    {
1191 1
        $fname = $func[1];
1192 1
        if ($func[2][0] !== 'list') {
1193
            return false;
1194 1
        } // need a list of arguments
1195 1
        /** @var array $rawComponents */
1196 1
        $rawComponents = $func[2][2];
1197 1
1198
        if ($fname === 'hsl' || $fname === 'hsla') {
1199 1
            $hsl = ['hsl'];
1200
            $i = 0;
1201
            foreach ($rawComponents as $c) {
1202 1
                $val = $this->reduce($c);
1203 1
                $val = isset($val[1]) ? (float) $val[1] : 0;
1204
1205
                if ($i === 0) {
1206 1
                    $clamp = 360;
1207
                } elseif ($i < 3) {
1208
                    $clamp = 100;
1209
                } else {
1210 1
                    $clamp = 1;
1211 24
                }
1212 4
1213 4
                $hsl[] = $this->converter->clamp($val, $clamp);
1214 4
                $i++;
1215 4
            }
1216 4
1217 4
            while (count($hsl) < 4) {
1218 1
                $hsl[] = 0;
1219
            }
1220 4
1221
            return $this->converter->toRGB($hsl);
1222 4
        } elseif ($fname === 'rgb' || $fname === 'rgba') {
1223 4
            $components = [];
1224
            $i = 1;
1225
            foreach ($rawComponents as $c) {
1226 4
                $c = $this->reduce($c);
1227
                if ($i < 4) {
1228
                    if ($c[0] === "number" && $c[2] === "%") {
1229
                        $components[] = 255 * ($c[1] / 100);
1230
                    } else {
1231
                        $components[] = (float) $c[1];
1232 4
                    }
1233
                } elseif ($i === 4) {
1234 4
                    if ($c[0] === "number" && $c[2] === "%") {
1235
                        $components[] = 1.0 * ($c[1] / 100);
1236
                    } else {
1237 4
                        $components[] = (float) $c[1];
1238
                    }
1239 4
                } else {
1240
                    break;
1241
                }
1242 23
1243
                $i++;
1244
            }
1245
            while (count($components) < 3) {
1246
                $components[] = 0;
1247
            }
1248
            array_unshift($components, 'color');
1249
1250
            return $this->fixColor($components);
1251 48
        }
1252
1253 48
        return false;
1254 48
    }
1255 7
1256 7
    /**
1257 7
     * @param array $value
1258
     * @param bool  $forExpression
1259 7
     *
1260 1
     * @return array|bool|mixed|null // <!-- dafuq?
1261
     */
1262
    public function reduce(array $value, $forExpression = false)
1263 7
    {
1264 6
        switch ($value[0]) {
1265
            case "interpolate":
1266
                $reduced = $this->reduce($value[1]);
1267 7
                $var = $this->compileValue($reduced);
1268 48
                $res = $this->reduce(["variable", $this->vPrefix . $var]);
1269 28
1270 28
                if ($res[0] === "raw_color") {
1271 1
                    $res = $this->coerce->coerceColor($res);
1272 1
                }
1273
1274
                if (empty($value[2])) {
1275 28
                    $res = $this->functions->e($res);
1276
                }
1277 28
1278
                return $res;
1279
            case "variable":
1280
                $key = $value[1];
1281 28
                if (is_array($key)) {
1282 28
                    $key = $this->reduce($key);
1283 27
                    $key = $this->vPrefix . $this->compileValue($this->functions->e($key));
1284
                }
1285 27
1286 47
                $seen =& $this->env->seenNames;
1287 29
1288 26
                if (!empty($seen[$key])) {
1289
                    $this->throwError("infinite loop detected: $key");
1290
                }
1291 29
1292 47
                $seen[$key] = true;
1293 19
                $out = $this->reduce($this->get($key));
1294 47
                $seen[$key] = false;
1295 26
1296 26
                return $out;
1297 11
            case "list":
1298 11
                foreach ($value[2] as &$item) {
1299 11
                    $item = $this->reduce($item, $forExpression);
1300 26
                }
1301
1302
                return $value;
1303
            case "expression":
1304
                return $this->evaluate($value);
1305 26
            case "string":
1306 45
                foreach ($value[2] as &$part) {
1307 4
                    if (is_array($part)) {
1308
                        $strip = $part[0] === "variable";
1309 4
                        $part = $this->reduce($part);
1310 45
                        if ($strip) {
1311 24
                            $part = $this->functions->e($part);
1312 24
                        }
1313 4
                    }
1314
                }
1315
1316 23
                return $value;
1317 23
            case "escape":
1318 1
                list(, $inner) = $value;
1319
1320
                return $this->functions->e($this->reduce($inner));
1321
            case "function":
1322 23
                $color = $this->funcToColor($value);
1323 23
                if ($color) {
1324 1
                    return $color;
1325
                }
1326
1327 23
                list(, $name, $args) = $value;
1328
                if ($name === "%") {
1329 23
                    $name = "_sprintf";
1330 14
                }
1331 14
1332
                // user functions
1333
                $f = null;
1334 14
                if (isset($this->libFunctions[$name]) && is_callable($this->libFunctions[$name])) {
1335 1
                    $f = $this->libFunctions[$name];
1336
                }
1337 13
1338
                $func = str_replace('-', '_', $name);
1339 9
1340
                if ($f !== null || method_exists($this->functions, $func)) {
1341 2
                    if ($args[0] === 'list') {
1342 2
                        $args = self::compressList($args[2], $args[1]);
1343
                    }
1344 2
1345 2
                    if ($f !== null) {
1346 2
                        $ret = $f($this->reduce($args, true), $this);
1347 2
                    } else {
1348
                        $ret = $this->functions->$func($this->reduce($args, true), $this);
1349
                    }
1350
                    if ($ret === null) {
1351
                        return [
1352
                            "string",
1353 8
                            "",
1354 3
                            [
1355 7
                                $name,
1356 2
                                "(",
1357
                                $args,
1358
                                ")",
1359 8
                            ],
1360
                        ];
1361
                    }
1362
1363 10
                    // convert to a typed value if the result is a php primitive
1364
                    if (is_numeric($ret)) {
1365 10
                        $ret = ['number', $ret, ""];
1366 40
                    } elseif (!is_array($ret)) {
1367 6
                        $ret = ['keyword', $ret];
1368 6
                    }
1369
1370 6
                    return $ret;
1371
                }
1372 6
1373
                // plain function, reduce args
1374 6
                $value[2] = $this->reduce($value[2]);
1375 6
1376
                return $value;
1377 6
            case "unary":
1378
                list(, $op, $exp) = $value;
1379
                $exp = $this->reduce($exp);
1380
1381
                if ($exp[0] === "number") {
1382
                    switch ($op) {
1383
                        case "+":
1384 40
                            return $exp;
1385 22
                        case "-":
1386 22
                            $exp[1] *= -1;
1387 5
1388 5
                            return $exp;
1389 2
                    }
1390
                }
1391 5
1392 22
                return ["string", "", [$op, $exp]];
1393 6
        }
1394
1395
        if ($forExpression) {
1396
            switch ($value[0]) {
1397 40
                case "keyword":
1398
                    $color = $this->coerce->coerceColor($value);
1399
                    if ($color !== null) {
1400
                        return $color;
1401
                    }
1402
                    break;
1403
                case "raw_color":
1404
                    return $this->coerce->coerceColor($value);
1405
            }
1406
        }
1407 2
1408
        return $value;
1409 2
    }
1410 2
1411
    /**
1412
     * turn list of length 1 into value type
1413 2
     *
1414
     * @param array $value
1415
     *
1416
     * @return array
1417
     */
1418
    protected function flattenList(array $value)
1419
    {
1420
        if ($value[0] === 'list' && count($value[2]) === 1) {
1421
            return $this->flattenList($value[2][0]);
1422
        }
1423 19
1424
        return $value;
1425 19
    }
1426
1427 19
    /**
1428 19
     * evaluate an expression
1429
     *
1430 19
     * @param array $exp
1431 19
     *
1432 5
     * @return array
1433
     */
1434
    protected function evaluate($exp)
1435 19
    {
1436 19
        list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
1437 5
1438
        $left = $this->reduce($left, true);
1439
        $right = $this->reduce($right, true);
1440 19
1441 19
        $leftColor = $this->coerce->coerceColor($left);
1442
        if ($leftColor !== null) {
1443
            $left = $leftColor;
1444 19
        }
1445
1446
        $rightColor = $this->coerce->coerceColor($right);
1447
        if ($rightColor !== null) {
1448 19
            $right = $rightColor;
1449 1
        }
1450
1451
        $ltype = $left[0];
1452 19
        $rtype = $right[0];
1453 19
1454 2
        // operators that work on all types
1455
        if ($op === "and") {
1456
            return $this->functions->toBool($left == self::$TRUE && $right == self::$TRUE);
1457
        }
1458 17
1459 17
        if ($op === "=") {
1460 17
            return $this->functions->toBool($this->equals($left, $right));
1461 17
        }
1462 17
1463
        $str = $this->stringConcatenate($left, $right);
1464
        if ($op === "+" && $str !== null) {
1465
            return $str;
1466
        }
1467 1
1468 1
        // type based operators
1469 1
        $fname = "op_${ltype}_${rtype}";
1470
        if (is_callable([$this, $fname])) {
1471 1
            $out = $this->$fname($op, $left, $right);
1472 1
            if ($out !== null) {
1473
                return $out;
1474
            }
1475 1
        }
1476
1477
        // make the expression look it did before being parsed
1478
        $paddedOp = $op;
1479
        if ($whiteBefore) {
1480
            $paddedOp = " " . $paddedOp;
1481
        }
1482
        if ($whiteAfter) {
1483
            $paddedOp .= " ";
1484
        }
1485
1486 19
        return ["string", "", [$left, $paddedOp, $right]];
1487 19
    }
1488 2
1489 2
    /**
1490
     * @param array $left
1491 2
     * @param array $right
1492
     *
1493 2
     * @return array|null
1494
     */
1495
    protected function stringConcatenate(array $left, array $right)
1496 18
    {
1497 18
        $strLeft = $this->coerce->coerceString($left);
1498 1
        if ($strLeft !== null) {
1499
            if ($right[0] === "string") {
1500 1
                $right[1] = "";
1501
            }
1502
            $strLeft[2][] = $right;
1503 17
1504
            return $strLeft;
1505
        }
1506
1507
        $strRight = $this->coerce->coerceString($right);
1508
        if ($strRight !== null) {
1509
            array_unshift($strRight[2], $left);
1510
1511
            return $strRight;
1512
        }
1513
1514
        return null;
1515
    }
1516 7
1517 7
1518
    /**
1519
     * make sure a color's components don't go out of bounds
1520 7
     *
1521 7
     * @param array $c
1522
     *
1523
     * @return mixed
1524
     */
1525 7
    public function fixColor(array $c)
1526
    {
1527
        foreach (range(1, 3) as $i) {
1528
            if ($c[$i] < 0) {
1529
                $c[$i] = 0;
1530
            }
1531
            if ($c[$i] > 255) {
1532
                $c[$i] = 255;
1533
            }
1534
        }
1535
1536
        return $c;
1537
    }
1538 1
1539 1
    /**
1540
     * @param string $op
1541
     * @param array  $lft
1542 1
     * @param array  $rgt
1543
     *
1544
     * @return array|null
1545
     * @throws \LesserPhp\Exception\GeneralException
1546
     */
1547
    protected function op_number_color($op, array $lft, array $rgt)
1548
    {
1549
        if ($op === '+' || $op === '*') {
1550
            return $this->op_color_number($op, $rgt, $lft);
1551
        }
1552
1553
        return null;
1554
    }
1555 2
1556
    /**
1557
     * @param string $op
1558
     * @param array  $lft
1559 2
     * @param array  $rgt
1560
     *
1561
     * @return array
1562 2
     * @throws \LesserPhp\Exception\GeneralException
1563
     */
1564
    protected function op_color_number($op, array $lft, array $rgt)
1565
    {
1566
        if ($rgt[0] === '%') {
1567
            $rgt[1] /= 100;
1568
        }
1569
1570
        return $this->op_color_color(
1571
            $op,
1572
            $lft,
1573
            array_fill(1, count($lft) - 1, $rgt[1])
1574
        );
1575
    }
1576
1577 5
    /**
1578 5
     * @param string $op
1579 5
     * @param        array
1580 5
     * $left
1581 5
     * @param array  $right
1582
     *
1583 5
     * @return array
1584 5
     * @throws \LesserPhp\Exception\GeneralException
1585 5
     */
1586 1
    protected function op_color_color($op, array $left, array $right)
1587 1
    {
1588 1
        $out = ['color'];
1589 1
        $max = count($left) > count($right) ? count($left) : count($right);
1590 1
        foreach (range(1, $max - 1) as $i) {
1591 1
            $lval = isset($left[$i]) ? $left[$i] : 0;
1592 1
            $rval = isset($right[$i]) ? $right[$i] : 0;
1593 1
            switch ($op) {
1594 1
                case '+':
1595 1
                    $out[] = $lval + $rval;
1596 1
                    break;
1597
                case '-':
1598
                    $out[] = $lval - $rval;
1599 1
                    break;
1600 1
                case '*':
1601
                    $out[] = $lval * $rval;
1602 5
                    break;
1603
                case '%':
1604
                    $out[] = $lval % $rval;
1605
                    break;
1606 5
                case '/':
1607
                    if ($rval == 0) {
1608
                        throw new GeneralException("evaluate error: can't divide by zero");
1609
                    }
1610
                    $out[] = $lval / $rval;
1611
                    break;
1612
                default:
1613
                    throw new GeneralException('evaluate error: color op number failed on op ' . $op);
1614
            }
1615
        }
1616
1617
        return $this->fixColor($out);
1618
    }
1619
1620
    /**
1621 15
     * operator on two numbers
1622
     *
1623 15
     * @param string $op
1624
     * @param array  $left
1625 15
     * @param array  $right
1626 9
     *
1627 9
     * @return array
1628 12
     * @throws \LesserPhp\Exception\GeneralException
1629 8
     */
1630 8
    protected function op_number_number($op, $left, $right)
1631 10
    {
1632 6
        $unit = empty($left[2]) ? $right[2] : $left[2];
1633 6
1634 9
        $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...
1635 1
        switch ($op) {
1636 1
            case '+':
1637 8
                $value = $left[1] + $right[1];
1638 4
                break;
1639
            case '*':
1640
                $value = $left[1] * $right[1];
1641 4
                break;
1642 4
            case '-':
1643 5
                $value = $left[1] - $right[1];
1644 1
                break;
1645 5
            case '%':
1646 3
                $value = $left[1] % $right[1];
1647 3
                break;
1648 1
            case '/':
1649 2
                if ($right[1] == 0) {
1650 2
                    throw new GeneralException('parse error: divide by zero');
1651
                }
1652
                $value = $left[1] / $right[1];
1653
                break;
1654
            case '<':
1655 13
                return $this->functions->toBool($left[1] < $right[1]);
1656
            case '>':
1657
                return $this->functions->toBool($left[1] > $right[1]);
1658
            case '>=':
1659
                return $this->functions->toBool($left[1] >= $right[1]);
1660
            case '=<':
1661
                return $this->functions->toBool($left[1] <= $right[1]);
1662
            default:
1663
                throw new GeneralException('parse error: unknown number operator: ' . $op);
1664
        }
1665
1666 49
        return ['number', $value, $unit];
1667 49
    }
1668 49
1669 49
    /**
1670 49
     * @param      $type
1671 49
     * @param null $selectors
1672
     *
1673 49
     * @return \stdClass
1674
     */
1675
    protected function makeOutputBlock($type, $selectors = null)
1676
    {
1677
        $b = new \stdClass();
1678
        $b->lines = [];
1679
        $b->children = [];
1680
        $b->selectors = $selectors;
1681
        $b->type = $type;
1682
        $b->parent = $this->scope;
1683
1684 49
        return $b;
1685 49
    }
1686 49
1687 49
    /**
1688
     * @param \LesserPhp\NodeEnv $parent
1689 49
     * @param Block|null $block
1690
     *
1691 49
     * @return \LesserPhp\NodeEnv
1692
     */
1693
    protected function pushEnv($parent, Block $block = null)
1694
    {
1695
        $e = new NodeEnv();
1696
        $e->setParent($parent);
1697
        $e->setBlock($block);
0 ignored issues
show
Documentation introduced by
$block is of type null|object<LesserPhp\Block>, but the function expects a string.

It seems like the type of the argument is not accepted by the function/method which you are calling.

In some cases, in particular if PHP’s automatic type-juggling kicks in this might be fine. In other cases, however this might be a bug.

We suggest to add an explicit type cast like in the following example:

function acceptsInteger($int) { }

$x = '123'; // string "123"

// Instead of
acceptsInteger($x);

// we recommend to use
acceptsInteger((integer) $x);
Loading history...
1698
        $e->setStore([]);
1699
1700
        $this->env = $e;
1701 40
1702 40
        return $e;
1703
    }
1704 40
1705
    /**
1706
     * pop something off the stack
1707
     *
1708
     * @return \LesserPhp\NodeEnv
1709
     */
1710
    protected function popEnv()
1711
    {
1712
        $old = $this->env;
1713
        $this->env = $this->env->getParent();
1714
1715 27
        return $old;
1716 27
    }
1717
1718
    /**
1719
     * set something in the current env
1720
     *
1721
     * @param $name
1722
     * @param $value
1723
     */
1724
    protected function set($name, $value)
1725
    {
1726
        $this->env->addStore($name, $value);
1727
    }
1728 28
1729
    /**
1730
     * get the highest occurrence entry for a name
1731 28
     *
1732
     * @param $name
1733 28
     *
1734 28
     * @return array
1735 28
     * @throws \LesserPhp\Exception\GeneralException
1736 3
     */
1737
    protected function get($name)
1738
    {
1739 28
        $current = $this->env;
1740 27
1741
        // track scope to evaluate
1742
        $scopeSecondary = [];
1743 19
1744
        $isArguments = $name === $this->vPrefix . 'arguments';
1745
        while ($current) {
1746
            if ($isArguments && count($current->getArguments()) > 0) {
1747 19
                return ['list', ' ', $current->getArguments()];
1748 19
            }
1749
1750 1
            if (isset($current->getStore()[$name])) {
1751
                return $current->getStore()[$name];
1752
            }
1753
            // has secondary scope?
1754 1
            if (isset($current->storeParent)) {
1755
                $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...
1756
            }
1757
1758
            if ($current->getParent() !== null) {
1759
                $current = $current->getParent();
1760
            } else {
1761
                $current = null;
1762
            }
1763
        }
1764
1765
        while (count($scopeSecondary)) {
1766
            // pop one off
1767
            $current = array_shift($scopeSecondary);
1768
            while ($current) {
1769
                if ($isArguments && isset($current->arguments)) {
1770
                    return ['list', ' ', $current->arguments];
1771
                }
1772
1773
                if (isset($current->store[$name])) {
1774
                    return $current->store[$name];
1775
                }
1776
1777
                // has secondary scope?
1778
                if (isset($current->storeParent)) {
1779 1
                    $scopeSecondary[] = $current->storeParent;
1780
                }
1781
1782
                if (isset($current->parent)) {
1783
                    $current = $current->parent;
1784
                } else {
1785
                    $current = null;
1786
                }
1787
            }
1788
        }
1789
1790
        throw new GeneralException("variable $name is undefined");
1791 1
    }
1792 1
1793 1
    /**
1794 1
     * inject array of unparsed strings into environment as variables
1795 1
     *
1796
     * @param string[] $args
1797 1
     *
1798 1
     * @throws \LesserPhp\Exception\GeneralException
1799 1
     */
1800
    protected function injectVariables(array $args)
1801
    {
1802
        $this->pushEnv($this->env);
1803 1
        $parser = new Parser($this, __METHOD__);
1804
        foreach ($args as $name => $strValue) {
1805 1
            if ($name{0} !== '@') {
1806
                $name = '@' . $name;
1807
            }
1808
            $parser->count = 0;
1809
            $parser->buffer = (string) $strValue;
1810
            if (!$parser->propertyValue($value)) {
1811
                throw new GeneralException("failed to parse passed in variable $name: $strValue");
1812
            }
1813
1814
            $this->set($name, $value);
1815
        }
1816 49
    }
1817 49
1818
    /**
1819 49
     * @param string $string
1820 49
     * @param string $name
1821
     *
1822 49
     * @return string
1823 49
     * @throws \LesserPhp\Exception\GeneralException
1824 49
     */
1825
    public function compile($string, $name = null)
1826 49
    {
1827
        $locale = setlocale(LC_NUMERIC, 0);
1828 49
        setlocale(LC_NUMERIC, 'C');
1829 1
1830
        $this->parser = $this->makeParser($name);
1831
        $root = $this->parser->parse($string);
1832 49
1833 49
        $this->env = null;
1834
        $this->scope = null;
1835 38
        $this->allParsedFiles = [];
1836 38
1837 38
        $this->formatter = $this->newFormatter();
1838 38
1839
        if (!empty($this->registeredVars)) {
1840 38
            $this->injectVariables($this->registeredVars);
1841
        }
1842
1843
        $this->sourceParser = $this->parser; // used for error messages
1844
        $this->compileBlock($root);
1845
1846
        ob_start();
1847
        $this->formatter->block($this->scope);
1848
        $out = ob_get_clean();
1849
        setlocale(LC_NUMERIC, $locale);
1850
1851
        return $out;
1852 1
    }
1853
1854
    /**
1855
     * @param string $fname
1856 1
     * @param string $outFname
1857
     *
1858 1
     * @return int|string
1859
     * @throws \LesserPhp\Exception\GeneralException
1860 1
     */
1861
    public function compileFile($fname, $outFname = null)
1862 1
    {
1863
        if (!is_readable($fname)) {
1864 1
            throw new GeneralException('load error: failed to find ' . $fname);
1865
        }
1866 1
1867
        $pi = pathinfo($fname);
1868 1
1869
        $oldImport = $this->importDirs;
1870
1871
        $this->importDirs[] = $pi['dirname'] . '/';
1872 1
1873
        $this->addParsedFile($fname);
1874
1875
        $out = $this->compile(file_get_contents($fname), $fname);
1876
1877
        $this->importDirs = $oldImport;
1878
1879
        if ($outFname !== null) {
1880
            return file_put_contents($outFname, $out);
1881
        }
1882
1883
        return $out;
1884
    }
1885
1886
    /**
1887 1
     * Based on explicit input/output files does a full change check on cache before compiling.
1888
     *
1889
     * @param string  $in
1890 1
     * @param string  $out
1891
     * @param boolean $force
1892
     *
1893
     * @return string Compiled CSS results
1894 1
     * @throws GeneralException
1895 1
     */
1896 1
    public function checkedCachedCompile($in, $out, $force = false)
1897
    {
1898
        if (!is_file($in) || !is_readable($in)) {
1899
            throw new GeneralException('Invalid or unreadable input file specified.');
1900 1
        }
1901
        if (is_dir($out) || !is_writable(file_exists($out) ? $out : dirname($out))) {
1902 1
            throw new GeneralException('Invalid or unwritable output file specified.');
1903 1
        }
1904 1
1905 1
        $outMeta = $out . '.meta';
1906 1
        $metadata = null;
1907
        if (!$force && is_file($outMeta)) {
1908
            $metadata = unserialize(file_get_contents($outMeta));
1909
        }
1910
1911 1
        $output = $this->cachedCompile($metadata ?: $in);
1912
1913
        if (!$metadata || $metadata['updated'] != $output['updated']) {
1914
            $css = $output['compiled'];
1915
            unset($output['compiled']);
1916
            file_put_contents($out, $css);
1917
            file_put_contents($outMeta, serialize($output));
1918
        } else {
1919
            $css = file_get_contents($out);
1920
        }
1921
1922
        return $css;
1923
    }
1924
1925
    /**
1926
     * compile only if changed input has changed or output doesn't exist
1927
     *
1928
     * @param string $in
1929
     * @param string $out
1930
     *
1931
     * @return bool
1932
     * @throws \LesserPhp\Exception\GeneralException
1933
     */
1934
    public function checkedCompile($in, $out)
1935
    {
1936
        if (!is_file($out) || filemtime($in) > filemtime($out)) {
1937
            $this->compileFile($in, $out);
1938
1939
            return true;
1940
        }
1941
1942
        return false;
1943
    }
1944
1945
    /**
1946
     * Execute lessphp on a .less file or a lessphp cache structure
1947
     *
1948
     * The lessphp cache structure contains information about a specific
1949
     * less file having been parsed. It can be used as a hint for future
1950
     * calls to determine whether or not a rebuild is required.
1951
     *
1952
     * The cache structure contains two important keys that may be used
1953
     * externally:
1954
     *
1955
     * compiled: The final compiled CSS
1956
     * updated: The time (in seconds) the CSS was last compiled
1957
     *
1958
     * The cache structure is a plain-ol' PHP associative array and can
1959 1
     * be serialized and unserialized without a hitch.
1960
     *
1961 1
     * @param mixed $in    Input
1962 1
     * @param bool  $force Force rebuild?
1963
     *
1964
     * @return array lessphp cache structure
1965
     * @throws \LesserPhp\Exception\GeneralException
1966
     */
1967
    public function cachedCompile($in, $force = false)
1968
    {
1969
        // assume no root
1970
        $root = null;
1971
1972
        if (is_string($in)) {
1973
            $root = $in;
1974
        } elseif (is_array($in) && isset($in['root'])) {
1975
            if ($force || !isset($in['files'])) {
1976
                // If we are forcing a recompile or if for some reason the
1977
                // structure does not contain any file information we should
1978
                // specify the root to trigger a rebuild.
1979
                $root = $in['root'];
1980
            } elseif (isset($in['files']) && is_array($in['files'])) {
1981
                foreach ($in['files'] as $fname => $ftime) {
1982
                    if (!file_exists($fname) || filemtime($fname) > $ftime) {
1983
                        // One of the files we knew about previously has changed
1984
                        // so we should look at our incoming root again.
1985 1
                        $root = $in['root'];
1986
                        break;
1987 1
                    }
1988 1
                }
1989 1
            }
1990 1
        } else {
1991 1
            // TODO: Throw an exception? We got neither a string nor something
1992
            // that looks like a compatible lessphp cache structure.
1993 1
            return null;
1994
        }
1995
1996
        if ($root !== null) {
1997
            // If we have a root value which means we should rebuild.
1998
            $out = [];
1999
            $out['root'] = $root;
2000
            $out['compiled'] = $this->compileFile($root);
2001
            $out['files'] = $this->allParsedFiles;
2002
            $out['updated'] = time();
2003
2004
            return $out;
2005
        } else {
2006
            // No changes, pass back the structure
2007
            // we were given initially.
2008
            return $in;
2009
        }
2010
    }
2011
2012
    /**
2013
     * parse and compile buffer
2014 37
     * This is deprecated
2015
     *
2016
     * @param null $str
2017
     * @param null $initialVariables
2018
     *
2019 37
     * @return int|string
2020 37
     * @throws \LesserPhp\Exception\GeneralException
2021 1
     * @deprecated
2022
     */
2023
    public function parse($str = null, $initialVariables = null)
2024 37
    {
2025
        if (is_array($str)) {
2026
            $initialVariables = $str;
2027 37
            $str = null;
2028
        }
2029
2030 37
        $oldVars = $this->registeredVars;
2031
        if ($initialVariables !== null) {
2032 37
            $this->setVariables($initialVariables);
2033
        }
2034
2035
        if ($str === null) {
2036
            throw new GeneralException('nothing to parse');
2037
        } else {
2038
            $out = $this->compile($str);
2039
        }
2040
2041
        $this->registeredVars = $oldVars;
2042 49
2043 49
        return $out;
2044
    }
2045 49
2046
    /**
2047
     * @param string $name
2048
     *
2049
     * @return \LesserPhp\Parser
2050
     */
2051
    protected function makeParser($name)
2052
    {
2053 1
        $parser = new Parser($this, $name);
2054 1
        $parser->setWriteComments($this->preserveComments);
2055
2056
        return $parser;
2057
    }
2058 14
2059 14
    /**
2060
     * @param string $name
2061
     */
2062
    public function setFormatter($name)
2063
    {
2064
        $this->formatterName = $name;
2065
    }
2066 49
2067 49
    public function setFormatterClass($formatter)
2068 1
    {
2069
        $this->formatter = $formatter;
2070
    }
2071 1
2072
    /**
2073
     * @return \LesserPhp\Formatter\FormatterInterface
2074 49
     */
2075
    protected function newFormatter()
2076 49
    {
2077
        $className = 'Lessjs';
2078
        if (!empty($this->formatterName)) {
2079
            if (!is_string($this->formatterName)) {
2080
                return $this->formatterName;
2081
            }
2082
            $className = $this->formatterName;
2083
        }
2084 1
2085 1
        $className = '\LesserPhp\Formatter\\' . $className;
2086
2087
        return new $className;
2088
    }
2089
2090
    /**
2091
     * @param bool $preserve
2092
     */
2093 1
    public function setPreserveComments($preserve)
2094 1
    {
2095
        $this->preserveComments = $preserve;
2096
    }
2097
2098
    /**
2099
     * @param string   $name
2100
     * @param callable $func
2101 1
     */
2102 1
    public function registerFunction($name, callable $func)
2103
    {
2104
        $this->libFunctions[$name] = $func;
2105
    }
2106
2107
    /**
2108
     * @param string $name
2109 1
     */
2110 1
    public function unregisterFunction($name)
2111
    {
2112
        unset($this->libFunctions[$name]);
2113
    }
2114
2115
    /**
2116
     * @param array $variables
2117
     */
2118
    public function setVariables(array $variables)
2119
    {
2120
        $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...
2121
    }
2122
2123
    /**
2124
     * @param $name
2125 38
     */
2126 38
    public function unsetVariable($name)
2127
    {
2128
        unset($this->registeredVars[$name]);
2129
    }
2130
2131
    /**
2132
     * @param string[] $dirs
2133
     */
2134
    public function setImportDirs(array $dirs)
2135
    {
2136
        $this->importDirs = $dirs;
2137
    }
2138
2139
    /**
2140
     * @param string $dir
2141 4
     */
2142
    public function addImportDir($dir)
2143
    {
2144
        $this->importDirs[] = $dir;
2145
    }
2146
2147
    /**
2148
     * @return string[]
2149 2
     */
2150 2
    public function getImportDirs()
2151
    {
2152
        return $this->importDirs;
2153
    }
2154
2155
    /**
2156
     * @param string $file
2157
     */
2158
    public function addParsedFile($file)
2159
    {
2160
        $this->allParsedFiles[realpath($file)] = filemtime($file);
2161
    }
2162
2163
    /**
2164
     * Uses the current value of $this->count to show line and line number
2165
     *
2166
     * @param string $msg
2167
     *
2168
     * @throws GeneralException
2169
     */
2170
    public function throwError($msg = null)
2171
    {
2172
        if ($this->sourceLoc >= 0) {
2173
            $this->sourceParser->throwError($msg, $this->sourceLoc);
2174
        }
2175
        throw new GeneralException($msg);
2176
    }
2177
2178
    /**
2179
     * compile file $in to file $out if $in is newer than $out
2180
     * returns true when it compiles, false otherwise
2181
     *
2182
     * @param                          $in
2183
     * @param                          $out
2184
     * @param \LesserPhp\Compiler|null $less
2185
     *
2186
     * @return bool
2187
     * @throws \LesserPhp\Exception\GeneralException
2188
     */
2189
    public static function ccompile($in, $out, Compiler $less = null)
2190
    {
2191
        if ($less === null) {
2192
            $less = new self;
2193
        }
2194
2195
        return $less->checkedCompile($in, $out);
2196
    }
2197
2198
    /**
2199
     * @param                          $in
2200
     * @param bool                     $force
2201
     * @param \LesserPhp\Compiler|null $less
2202
     *
2203
     * @return array
2204
     * @throws \LesserPhp\Exception\GeneralException
2205
     */
2206
    public static function cexecute($in, $force = false, Compiler $less = null)
2207
    {
2208
        if ($less === null) {
2209
            $less = new self;
2210
        }
2211 49
2212
        return $less->cachedCompile($in, $force);
2213
    }
2214
2215
    /**
2216
     * Import Css
2217
     *
2218
     * Set allowing importing (and not compiling)
2219
     *
2220
     * @param bool $true (optional) Default, allow CSS.
2221 46
     *
2222
     * @return void
2223
     *
2224
     * @access public 
2225
     *
2226
     * @author Michael Mulligan <[email protected]> 
2227
     */
2228
    public function importCss($true = null)
2229 3
    {
2230
        if ($true === null) {
2231
            return $this->import_css;
2232
        } else {
2233
            $this->import_css = (bool) $true;
2234
        }
2235
    }
2236
2237
    /**
2238
     * prefix of abstract properties
2239
     *
2240
     * @return string
2241
     */
2242
    public function getVPrefix()
2243
    {
2244
        return $this->vPrefix;
2245
    }
2246
2247
    /**
2248
     * prefix of abstract blocks
2249
     *
2250 1
     * @return string
2251 1
     */
2252
    public function getMPrefix()
2253
    {
2254
        return $this->mPrefix;
2255
    }
2256
2257
    /**
2258 3
     * @return string
2259
     */
2260 1
    public function getParentSelector()
2261
    {
2262
        return $this->parentSelector;
2263
    }
2264
2265
    /**
2266
     * @param int $numberPresicion
2267
     */
2268
    protected function setNumberPrecision($numberPresicion = null)
2269
    {
2270
        $this->numberPrecision = $numberPresicion;
2271
    }
2272
2273
    /**
2274
     * @return \LesserPhp\Library\Coerce
2275
     */
2276
    protected function getCoerce()
2277
    {
2278
        return $this->coerce;
2279
    }
2280
2281
    public function setImportDisabled()
2282
    {
2283
        $this->importDisabled = true;
2284
    }
2285
2286
    /**
2287
     * @return bool
2288
     */
2289
    public function isImportDisabled()
2290
    {
2291
        return $this->importDisabled;
2292
    }
2293
}
2294