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

Compiler::importCss()   A

Complexity

Conditions 2
Paths 2

Size

Total Lines 7
Code Lines 5

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 1
CRAP Score 2

Importance

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