lessc::compileProp()   F
last analyzed

Complexity

Conditions 26
Paths 222

Size

Total Lines 117
Code Lines 82

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 26
eloc 82
nc 222
nop 3
dl 0
loc 117
rs 3.9588
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * lessphp v0.3.9
5
 * http://leafo.net/lessphp.
6
 *
7
 * LESS css compiler, adapted from http://lesscss.org
8
 *
9
 * Copyright 2012, Leaf Corcoran <[email protected]>
10
 * Licensed under MIT or GPLv3, see LICENSE
11
 */
12
13
/**
14
 * The less compiler and parser.
15
 *
16
 * Converting LESS to CSS is a three stage process. The incoming file is parsed
17
 * by `lessc_parser` into a syntax tree, then it is compiled into another tree
18
 * representing the CSS structure by `lessc`. The CSS tree is fed into a
19
 * formatter, like `lessc_formatter` which then outputs CSS as a string.
20
 *
21
 * During the first compile, all values are *reduced*, which means that their
22
 * types are brought to the lowest form before being dump as strings. This
23
 * handles math equations, variable dereferences, and the like.
24
 *
25
 * The `parse` function of `lessc` is the entry point.
26
 *
27
 * In summary:
28
 *
29
 * The `lessc` class creates an intstance of the parser, feeds it LESS code,
30
 * then transforms the resulting tree to a CSS tree. This class also holds the
31
 * evaluation context, such as all available mixins and variables at any given
32
 * time.
33
 *
34
 * The `lessc_parser` class is only concerned with parsing its input.
35
 *
36
 * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
37
 * handling things like indentation.
38
 */
39
class lessc
40
{
41
    public static $VERSION = 'v0.3.9';
42
    protected static $TRUE = ['keyword', 'true'];
43
    protected static $FALSE = ['keyword', 'false'];
44
45
    protected $libFunctions = [];
46
    protected $registeredVars = [];
47
    protected $preserveComments = false;
48
49
    public $vPrefix = '@'; // prefix of abstract properties
50
    public $mPrefix = '$'; // prefix of abstract blocks
51
    public $parentSelector = '&';
52
53
    public $importDisabled = false;
54
    public $importDir = '';
55
56
    protected $numberPrecision = null;
57
58
    // set to the parser that generated the current line when compiling
59
    // so we know how to create error messages
60
    protected $sourceParser = null;
61
    protected $sourceLoc = null;
62
63
    public static $defaultValue = ['keyword', ''];
64
65
    protected static $nextImportId = 0; // uniquely identify imports
66
67
    // attempts to find the path of an import url, returns null for css files
68
    protected function findImport($url)
69
    {
70
        foreach ((array) $this->importDir as $dir) {
71
            $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url;
72
            if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) {
73
                return $file;
74
            }
75
        }
76
    }
77
78
    protected function fileExists($name)
79
    {
80
        return is_file($name);
81
    }
82
83
    public static function compressList($items, $delim)
84
    {
85
        if (!isset($items[1]) && isset($items[0])) {
86
            return $items[0];
87
        } else {
88
            return ['list', $delim, $items];
89
        }
90
    }
91
92
    public static function preg_quote($what)
93
    {
94
        return preg_quote($what, '/');
95
    }
96
97
    protected function tryImport($importPath, $parentBlock, $out)
98
    {
99
        if ($importPath[0] == 'function' && $importPath[1] == 'url') {
100
            $importPath = $this->flattenList($importPath[2]);
101
        }
102
103
        $str = $this->coerceString($importPath);
104
        if ($str === null) {
105
            return false;
106
        }
107
108
        $url = $this->compileValue($this->lib_e($str));
109
110
        // don't import if it ends in css
111
        if (substr_compare($url, '.css', -4, 4) === 0) {
112
            return false;
113
        }
114
115
        $realPath = $this->findImport($url);
116
        if ($realPath === null) {
117
            return false;
118
        }
119
120
        if ($this->importDisabled) {
121
            return [false, '/* import disabled */'];
122
        }
123
124
        $this->addParsedFile($realPath);
125
        $parser = $this->makeParser($realPath);
126
        $root = $parser->parse(file_get_contents($realPath));
127
128
        // set the parents of all the block props
129
        foreach ($root->props as $prop) {
130
            if ($prop[0] == 'block') {
131
                $prop[1]->parent = $parentBlock;
132
            }
133
        }
134
135
        // copy mixins into scope, set their parents
136
        // bring blocks from import into current block
137
        // TODO: need to mark the source parser	these came from this file
138
        foreach ($root->children as $childName => $child) {
139
            if (isset($parentBlock->children[$childName])) {
140
                $parentBlock->children[$childName] = array_merge(
141
                    $parentBlock->children[$childName],
142
                    $child);
143
            } else {
144
                $parentBlock->children[$childName] = $child;
145
            }
146
        }
147
148
        $pi = pathinfo($realPath);
149
        $dir = $pi['dirname'];
150
151
        list($top, $bottom) = $this->sortProps($root->props, true);
152
        $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir);
153
154
        return [true, $bottom, $parser, $dir];
155
    }
156
157
    protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir)
0 ignored issues
show
Unused Code introduced by
The parameter $sourceParser is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
158
    {
159
        $oldSourceParser = $this->sourceParser;
160
161
        $oldImport = $this->importDir;
162
163
        // TODO: this is because the importDir api is stupid
164
        $this->importDir = (array) $this->importDir;
0 ignored issues
show
Documentation Bug introduced by
It seems like (array) $this->importDir of type array is incompatible with the declared type string of property $importDir.

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...
165
        array_unshift($this->importDir, $importDir);
166
167
        foreach ($props as $prop) {
168
            $this->compileProp($prop, $block, $out);
169
        }
170
171
        $this->importDir = $oldImport;
172
        $this->sourceParser = $oldSourceParser;
173
    }
174
175
    /**
176
     * Recursively compiles a block.
177
     *
178
     * A block is analogous to a CSS block in most cases. A single LESS document
179
     * is encapsulated in a block when parsed, but it does not have parent tags
180
     * so all of it's children appear on the root level when compiled.
181
     *
182
     * Blocks are made up of props and children.
183
     *
184
     * Props are property instructions, array tuples which describe an action
185
     * to be taken, eg. write a property, set a variable, mixin a block.
186
     *
187
     * The children of a block are just all the blocks that are defined within.
188
     * This is used to look up mixins when performing a mixin.
189
     *
190
     * Compiling the block involves pushing a fresh environment on the stack,
191
     * and iterating through the props, compiling each one.
192
     *
193
     * See lessc::compileProp()
194
     */
195
    protected function compileBlock($block)
196
    {
197
        switch ($block->type) {
198
            case 'root':
199
                $this->compileRoot($block);
200
                break;
201
            case null:
202
                $this->compileCSSBlock($block);
203
                break;
204
            case 'media':
205
                $this->compileMedia($block);
206
                break;
207
            case 'directive':
208
                $name = '@'.$block->name;
209
                if (!empty($block->value)) {
210
                    $name .= ' '.$this->compileValue($this->reduce($block->value));
211
                }
212
213
                $this->compileNestedBlock($block, [$name]);
214
                break;
215
            default:
216
                $this->throwError("unknown block type: $block->type\n");
217
        }
218
    }
219
220
    protected function compileCSSBlock($block)
221
    {
222
        $env = $this->pushEnv();
223
224
        $selectors = $this->compileSelectors($block->tags);
225
        $env->selectors = $this->multiplySelectors($selectors);
226
        $out = $this->makeOutputBlock(null, $env->selectors);
227
228
        $this->scope->children[] = $out;
0 ignored issues
show
Bug introduced by
The property scope does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
229
        $this->compileProps($block, $out);
230
231
        $block->scope = $env; // mixins carry scope with them!
232
        $this->popEnv();
233
    }
234
235
    protected function compileMedia($media)
236
    {
237
        $env = $this->pushEnv($media);
238
        $parentScope = $this->mediaParent($this->scope);
239
240
        $query = $this->compileMediaQuery($this->multiplyMedia($env));
241
242
        $this->scope = $this->makeOutputBlock($media->type, [$query]);
243
        $parentScope->children[] = $this->scope;
244
245
        $this->compileProps($media, $this->scope);
246
247
        if (count($this->scope->lines) > 0) {
248
            $orphanSelelectors = $this->findClosestSelectors();
249
            if (!is_null($orphanSelelectors)) {
250
                $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
251
                $orphan->lines = $this->scope->lines;
252
                array_unshift($this->scope->children, $orphan);
253
                $this->scope->lines = [];
254
            }
255
        }
256
257
        $this->scope = $this->scope->parent;
258
        $this->popEnv();
259
    }
260
261
    protected function mediaParent($scope)
262
    {
263
        while (!empty($scope->parent)) {
264
            if (!empty($scope->type) && $scope->type != 'media') {
265
                break;
266
            }
267
            $scope = $scope->parent;
268
        }
269
270
        return $scope;
271
    }
272
273
    protected function compileNestedBlock($block, $selectors)
274
    {
275
        $this->pushEnv($block);
276
        $this->scope = $this->makeOutputBlock($block->type, $selectors);
277
        $this->scope->parent->children[] = $this->scope;
278
279
        $this->compileProps($block, $this->scope);
280
281
        $this->scope = $this->scope->parent;
282
        $this->popEnv();
283
    }
284
285
    protected function compileRoot($root)
286
    {
287
        $this->pushEnv();
288
        $this->scope = $this->makeOutputBlock($root->type);
289
        $this->compileProps($root, $this->scope);
290
        $this->popEnv();
291
    }
292
293
    protected function compileProps($block, $out)
294
    {
295
        foreach ($this->sortProps($block->props) as $prop) {
296
            $this->compileProp($prop, $block, $out);
297
        }
298
    }
299
300
    protected function sortProps($props, $split = false)
301
    {
302
        $vars = [];
303
        $imports = [];
304
        $other = [];
305
306
        foreach ($props as $prop) {
307
            switch ($prop[0]) {
308
                case 'assign':
309
                    if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
310
                        $vars[] = $prop;
311
                    } else {
312
                        $other[] = $prop;
313
                    }
314
                    break;
315
                case 'import':
316
                    $id = self::$nextImportId++;
317
                    $prop[] = $id;
318
                    $imports[] = $prop;
319
                    $other[] = ['import_mixin', $id];
320
                    break;
321
                default:
322
                    $other[] = $prop;
323
            }
324
        }
325
326
        if ($split) {
327
            return [array_merge($vars, $imports), $other];
328
        } else {
329
            return array_merge($vars, $imports, $other);
330
        }
331
    }
332
333
    protected function compileMediaQuery($queries)
334
    {
335
        $compiledQueries = [];
336
        foreach ($queries as $query) {
337
            $parts = [];
338
            foreach ($query as $q) {
339
                switch ($q[0]) {
340
                    case 'mediaType':
341
                        $parts[] = implode(' ', array_slice($q, 1));
342
                        break;
343
                    case 'mediaExp':
344
                        if (isset($q[2])) {
345
                            $parts[] = "($q[1]: ".
346
                                $this->compileValue($this->reduce($q[2])).')';
347
                        } else {
348
                            $parts[] = "($q[1])";
349
                        }
350
                        break;
351
                    case 'variable':
352
                        $parts[] = $this->compileValue($this->reduce($q));
353
                        break;
354
                }
355
            }
356
357
            if (count($parts) > 0) {
358
                $compiledQueries[] = implode(' and ', $parts);
359
            }
360
        }
361
362
        $out = '@media';
363
        if (!empty($parts)) {
364
            $out .= ' '.
365
                implode($this->formatter->selectorSeparator, $compiledQueries);
0 ignored issues
show
Bug introduced by
The property formatter does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
366
        }
367
368
        return $out;
369
    }
370
371
    protected function multiplyMedia($env, $childQueries = null)
372
    {
373
        if (is_null($env) ||
374
            !empty($env->block->type) && $env->block->type != 'media'
375
        ) {
376
            return $childQueries;
377
        }
378
379
        // plain old block, skip
380
        if (empty($env->block->type)) {
381
            return $this->multiplyMedia($env->parent, $childQueries);
382
        }
383
384
        $out = [];
385
        $queries = $env->block->queries;
386
        if (is_null($childQueries)) {
387
            $out = $queries;
388
        } else {
389
            foreach ($queries as $parent) {
390
                foreach ($childQueries as $child) {
391
                    $out[] = array_merge($parent, $child);
392
                }
393
            }
394
        }
395
396
        return $this->multiplyMedia($env->parent, $out);
397
    }
398
399
    protected function expandParentSelectors(&$tag, $replace)
400
    {
401
        $parts = explode('$&$', $tag);
402
        $count = 0;
403
        foreach ($parts as &$part) {
404
            $part = str_replace($this->parentSelector, $replace, $part, $c);
405
            $count += $c;
406
        }
407
        $tag = implode($this->parentSelector, $parts);
408
409
        return $count;
410
    }
411
412
    protected function findClosestSelectors()
413
    {
414
        $env = $this->env;
0 ignored issues
show
Bug introduced by
The property env does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
415
        $selectors = null;
416
        while ($env !== null) {
417
            if (isset($env->selectors)) {
418
                $selectors = $env->selectors;
419
                break;
420
            }
421
            $env = $env->parent;
422
        }
423
424
        return $selectors;
425
    }
426
427
    // multiply $selectors against the nearest selectors in env
428
    protected function multiplySelectors($selectors)
429
    {
430
        // find parent selectors
431
432
        $parentSelectors = $this->findClosestSelectors();
433
        if (is_null($parentSelectors)) {
434
            // kill parent reference in top level selector
435
            foreach ($selectors as &$s) {
436
                $this->expandParentSelectors($s, '');
437
            }
438
439
            return $selectors;
440
        }
441
442
        $out = [];
443
        foreach ($parentSelectors as $parent) {
444
            foreach ($selectors as $child) {
445
                $count = $this->expandParentSelectors($child, $parent);
446
447
                // don't prepend the parent tag if & was used
448
                if ($count > 0) {
449
                    $out[] = trim($child);
450
                } else {
451
                    $out[] = trim($parent.' '.$child);
452
                }
453
            }
454
        }
455
456
        return $out;
457
    }
458
459
    // reduces selector expressions
460
    protected function compileSelectors($selectors)
461
    {
462
        $out = [];
463
464
        foreach ($selectors as $s) {
465
            if (is_array($s)) {
466
                list(, $value) = $s;
467
                $out[] = trim($this->compileValue($this->reduce($value)));
468
            } else {
469
                $out[] = $s;
470
            }
471
        }
472
473
        return $out;
474
    }
475
476
    protected function eq($left, $right)
477
    {
478
        return $left == $right;
479
    }
480
481
    protected function patternMatch($block, $callingArgs)
482
    {
483
        // match the guards if it has them
484
        // any one of the groups must have all its guards pass for a match
485
        if (!empty($block->guards)) {
486
            $groupPassed = false;
487
            foreach ($block->guards as $guardGroup) {
488
                foreach ($guardGroup as $guard) {
489
                    $this->pushEnv();
490
                    $this->zipSetArgs($block->args, $callingArgs);
491
492
                    $negate = false;
493
                    if ($guard[0] == 'negate') {
494
                        $guard = $guard[1];
495
                        $negate = true;
496
                    }
497
498
                    $passed = $this->reduce($guard) == self::$TRUE;
499
                    if ($negate) {
500
                        $passed = !$passed;
501
                    }
502
503
                    $this->popEnv();
504
505
                    if ($passed) {
506
                        $groupPassed = true;
507
                    } else {
508
                        $groupPassed = false;
509
                        break;
510
                    }
511
                }
512
513
                if ($groupPassed) {
514
                    break;
515
                }
516
            }
517
518
            if (!$groupPassed) {
519
                return false;
520
            }
521
        }
522
523
        $numCalling = count($callingArgs);
524
525
        if (empty($block->args)) {
526
            return $block->isVararg || $numCalling == 0;
527
        }
528
529
        $i = -1; // no args
530
        // try to match by arity or by argument literal
531
        foreach ($block->args as $i => $arg) {
532
            switch ($arg[0]) {
533
                case 'lit':
534
                    if (empty($callingArgs[$i]) || !$this->eq($arg[1], $callingArgs[$i])) {
535
                        return false;
536
                    }
537
                    break;
538
                case 'arg':
539
                    // no arg and no default value
540
                    if (!isset($callingArgs[$i]) && !isset($arg[2])) {
541
                        return false;
542
                    }
543
                    break;
544
                case 'rest':
545
                    $i--; // rest can be empty
546
                    break 2;
547
            }
548
        }
549
550
        if ($block->isVararg) {
551
            return true; // not having enough is handled above
552
        } else {
553
            $numMatched = $i + 1;
554
555
            // greater than becuase default values always match
556
            return $numMatched >= $numCalling;
557
        }
558
    }
559
560
    protected function patternMatchAll($blocks, $callingArgs)
561
    {
562
        $matches = null;
563
        foreach ($blocks as $block) {
564
            if ($this->patternMatch($block, $callingArgs)) {
565
                $matches[] = $block;
566
            }
567
        }
568
569
        return $matches;
570
    }
571
572
    // attempt to find blocks matched by path and args
573
    protected function findBlocks($searchIn, $path, $args, $seen = [])
574
    {
575
        if ($searchIn == null) {
576
            return;
577
        }
578
        if (isset($seen[$searchIn->id])) {
579
            return;
580
        }
581
        $seen[$searchIn->id] = true;
582
583
        $name = $path[0];
584
585
        if (isset($searchIn->children[$name])) {
586
            $blocks = $searchIn->children[$name];
587
            if (count($path) == 1) {
588
                $matches = $this->patternMatchAll($blocks, $args);
0 ignored issues
show
Bug introduced by
Are you sure the assignment to $matches is correct as $this->patternMatchAll($blocks, $args) (which targets lessc::patternMatchAll()) seems to always return null.

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

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

}

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

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

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

Loading history...
589
                if (!empty($matches)) {
590
                    // This will return all blocks that match in the closest
591
                    // scope that has any matching block, like lessjs
592
                    return $matches;
593
                }
594
            } else {
595
                $matches = [];
596
                foreach ($blocks as $subBlock) {
597
                    $subMatches = $this->findBlocks($subBlock,
598
                        array_slice($path, 1), $args, $seen);
599
600
                    if (!is_null($subMatches)) {
601
                        foreach ($subMatches as $sm) {
602
                            $matches[] = $sm;
603
                        }
604
                    }
605
                }
606
607
                return count($matches) > 0 ? $matches : null;
608
            }
609
        }
610
611
        if ($searchIn->parent === $searchIn) {
612
            return;
613
        }
614
615
        return $this->findBlocks($searchIn->parent, $path, $args, $seen);
616
    }
617
618
    // sets all argument names in $args to either the default value
619
    // or the one passed in through $values
620
    protected function zipSetArgs($args, $values)
621
    {
622
        $i = 0;
623
        $assignedValues = [];
624
        foreach ($args as $a) {
625
            if ($a[0] == 'arg') {
626
                if ($i < count($values) && !is_null($values[$i])) {
627
                    $value = $values[$i];
628
                } elseif (isset($a[2])) {
629
                    $value = $a[2];
630
                } else {
631
                    $value = null;
632
                }
633
634
                $value = $this->reduce($value);
635
                $this->set($a[1], $value);
636
                $assignedValues[] = $value;
637
            }
638
            $i++;
639
        }
640
641
        // check for a rest
642
        $last = end($args);
643
        if ($last[0] == 'rest') {
644
            $rest = array_slice($values, count($args) - 1);
645
            $this->set($last[1], $this->reduce(['list', ' ', $rest]));
646
        }
647
648
        $this->env->arguments = $assignedValues;
649
    }
650
651
    // compile a prop and update $lines or $blocks appropriately
652
    protected function compileProp($prop, $block, $out)
653
    {
654
        // set error position context
655
        $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
656
657
        switch ($prop[0]) {
658
            case 'assign':
659
                list(, $name, $value) = $prop;
660
                if ($name[0] == $this->vPrefix) {
661
                    $this->set($name, $value);
662
                } else {
663
                    $out->lines[] = $this->formatter->property($name,
664
                        $this->compileValue($this->reduce($value)));
665
                }
666
                break;
667
            case 'block':
668
                list(, $child) = $prop;
669
                $this->compileBlock($child);
670
                break;
671
            case 'mixin':
672
                list(, $path, $args, $suffix) = $prop;
673
674
                $args = array_map([$this, 'reduce'], (array) $args);
675
                $mixins = $this->findBlocks($block, $path, $args);
676
677
                if ($mixins === null) {
678
                    // fwrite(STDERR,"failed to find block: ".implode(" > ", $path)."\n");
679
                    break; // throw error here??
680
                }
681
682
                foreach ($mixins as $mixin) {
683
                    $haveScope = false;
684
                    if (isset($mixin->parent->scope)) {
685
                        $haveScope = true;
686
                        $mixinParentEnv = $this->pushEnv();
687
                        $mixinParentEnv->storeParent = $mixin->parent->scope;
688
                    }
689
690
                    $haveArgs = false;
691
                    if (isset($mixin->args)) {
692
                        $haveArgs = true;
693
                        $this->pushEnv();
694
                        $this->zipSetArgs($mixin->args, $args);
695
                    }
696
697
                    $oldParent = $mixin->parent;
698
                    if ($mixin != $block) {
699
                        $mixin->parent = $block;
700
                    }
701
702
                    foreach ($this->sortProps($mixin->props) as $subProp) {
703
                        if ($suffix !== null &&
704
                            $subProp[0] == 'assign' &&
705
                            is_string($subProp[1]) &&
706
                            $subProp[1][0] != $this->vPrefix
707
                        ) {
708
                            $subProp[2] = [
709
                                'list',
710
                                ' ',
711
                                [$subProp[2], ['keyword', $suffix]],
712
                            ];
713
                        }
714
715
                        $this->compileProp($subProp, $mixin, $out);
716
                    }
717
718
                    $mixin->parent = $oldParent;
719
720
                    if ($haveArgs) {
721
                        $this->popEnv();
722
                    }
723
                    if ($haveScope) {
724
                        $this->popEnv();
725
                    }
726
                }
727
728
                break;
729
            case 'raw':
730
                $out->lines[] = $prop[1];
731
                break;
732
            case 'directive':
733
                list(, $name, $value) = $prop;
734
                $out->lines[] = "@$name ".$this->compileValue($this->reduce($value)).';';
735
                break;
736
            case 'comment':
737
                $out->lines[] = $prop[1];
738
                break;
739
            case 'import':
740
                list(, $importPath, $importId) = $prop;
741
                $importPath = $this->reduce($importPath);
742
743
                if (!isset($this->env->imports)) {
744
                    $this->env->imports = [];
745
                }
746
747
                $result = $this->tryImport($importPath, $block, $out);
748
749
                $this->env->imports[$importId] = $result === false ?
750
                    [false, '@import '.$this->compileValue($importPath).';'] :
751
                    $result;
752
753
                break;
754
            case 'import_mixin':
755
                list(, $importId) = $prop;
756
                $import = $this->env->imports[$importId];
757
                if ($import[0] === false) {
758
                    $out->lines[] = $import[1];
759
                } else {
760
                    list(, $bottom, $parser, $importDir) = $import;
761
                    $this->compileImportedProps($bottom, $block, $out, $parser, $importDir);
762
                }
763
764
                break;
765
            default:
766
                $this->throwError("unknown op: {$prop[0]}\n");
767
        }
768
    }
769
770
    /**
771
     * Compiles a primitive value into a CSS property value.
772
     *
773
     * Values in lessphp are typed by being wrapped in arrays, their format is
774
     * typically:
775
     *
776
     *     array(type, contents [, additional_contents]*)
777
     *
778
     * The input is expected to be reduced. This function will not work on
779
     * things like expressions and variables.
780
     */
781
    protected function compileValue($value)
782
    {
783
        switch ($value[0]) {
784
            case 'list':
785
                // [1] - delimiter
786
                // [2] - array of values
787
                return implode($value[1], array_map([$this, 'compileValue'], $value[2]));
788
            case 'raw_color':
789
                if (!empty($this->formatter->compressColors)) {
790
                    return $this->compileValue($this->coerceColor($value));
791
                }
792
793
                return $value[1];
794
            case 'keyword':
795
                // [1] - the keyword
796
                return $value[1];
797
            case 'number':
798
                list(, $num, $unit) = $value;
799
                // [1] - the number
800
                // [2] - the unit
801
                if ($this->numberPrecision !== null) {
802
                    $num = round($num, $this->numberPrecision);
803
                }
804
805
                return $num.$unit;
806
            case 'string':
807
                // [1] - contents of string (includes quotes)
808
                list(, $delim, $content) = $value;
809
                foreach ($content as &$part) {
810
                    if (is_array($part)) {
811
                        $part = $this->compileValue($part);
812
                    }
813
                }
814
815
                return $delim.implode($content).$delim;
816
            case 'color':
817
                // [1] - red component (either number or a %)
818
                // [2] - green component
819
                // [3] - blue component
820
                // [4] - optional alpha component
821
                list(, $r, $g, $b) = $value;
822
                $r = round($r);
823
                $g = round($g);
824
                $b = round($b);
825
826
                if (count($value) == 5 && $value[4] != 1) { // rgba
827
                    return 'rgba('.$r.','.$g.','.$b.','.$value[4].')';
828
                }
829
830
                $h = sprintf('#%02x%02x%02x', $r, $g, $b);
831
832
                if (!empty($this->formatter->compressColors)) {
833
                    // Converting hex color to short notation (e.g. #003399 to #039)
834
                    if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
835
                        $h = '#'.$h[1].$h[3].$h[5];
836
                    }
837
                }
838
839
                return $h;
840
841
            case 'function':
842
                list(, $name, $args) = $value;
843
844
                return $name.'('.$this->compileValue($args).')';
845
            default: // assumed to be unit
846
                $this->throwError("unknown value type: $value[0]");
847
        }
848
    }
849
850
    protected function lib_isnumber($value)
851
    {
852
        return $this->toBool($value[0] == 'number');
853
    }
854
855
    protected function lib_isstring($value)
856
    {
857
        return $this->toBool($value[0] == 'string');
858
    }
859
860
    protected function lib_iscolor($value)
861
    {
862
        return $this->toBool($this->coerceColor($value));
863
    }
864
865
    protected function lib_iskeyword($value)
866
    {
867
        return $this->toBool($value[0] == 'keyword');
868
    }
869
870
    protected function lib_ispixel($value)
871
    {
872
        return $this->toBool($value[0] == 'number' && $value[2] == 'px');
873
    }
874
875
    protected function lib_ispercentage($value)
876
    {
877
        return $this->toBool($value[0] == 'number' && $value[2] == '%');
878
    }
879
880
    protected function lib_isem($value)
881
    {
882
        return $this->toBool($value[0] == 'number' && $value[2] == 'em');
883
    }
884
885
    protected function lib_isrem($value)
886
    {
887
        return $this->toBool($value[0] == 'number' && $value[2] == 'rem');
888
    }
889
890
    protected function lib_rgbahex($color)
891
    {
892
        $color = $this->coerceColor($color);
893
        if (is_null($color)) {
894
            $this->throwError('color expected for rgbahex');
895
        }
896
897
        return sprintf('#%02x%02x%02x%02x',
898
            isset($color[4]) ? $color[4] * 255 : 255,
899
            $color[1], $color[2], $color[3]);
900
    }
901
902
    protected function lib_argb($color)
903
    {
904
        return $this->lib_rgbahex($color);
905
    }
906
907
    // utility func to unquote a string
908
    protected function lib_e($arg)
909
    {
910
        switch ($arg[0]) {
911
            case 'list':
912
                $items = $arg[2];
913
                if (isset($items[0])) {
914
                    return $this->lib_e($items[0]);
915
                }
916
917
                return self::$defaultValue;
918
            case 'string':
919
                $arg[1] = '';
920
921
                return $arg;
922
            case 'keyword':
923
                return $arg;
924
            default:
925
                return ['keyword', $this->compileValue($arg)];
926
        }
927
    }
928
929
    protected function lib__sprintf($args)
930
    {
931
        if ($args[0] != 'list') {
932
            return $args;
933
        }
934
        $values = $args[2];
935
        $string = array_shift($values);
936
        $template = $this->compileValue($this->lib_e($string));
937
938
        $i = 0;
939
        if (preg_match_all('/%[dsa]/', $template, $m)) {
940
            foreach ($m[0] as $match) {
941
                $val = isset($values[$i]) ?
942
                    $this->reduce($values[$i]) : ['keyword', ''];
943
944
                // lessjs compat, renders fully expanded color, not raw color
945
                if ($color = $this->coerceColor($val)) {
946
                    $val = $color;
947
                }
948
949
                $i++;
950
                $rep = $this->compileValue($this->lib_e($val));
951
                $template = preg_replace('/'.self::preg_quote($match).'/',
952
                    $rep, $template, 1);
953
            }
954
        }
955
956
        $d = $string[0] == 'string' ? $string[1] : '"';
957
958
        return ['string', $d, [$template]];
959
    }
960
961
    protected function lib_floor($arg)
962
    {
963
        $value = $this->assertNumber($arg);
964
965
        return ['number', floor($value), $arg[2]];
966
    }
967
968
    protected function lib_ceil($arg)
969
    {
970
        $value = $this->assertNumber($arg);
971
972
        return ['number', ceil($value), $arg[2]];
973
    }
974
975
    protected function lib_round($arg)
976
    {
977
        $value = $this->assertNumber($arg);
978
979
        return ['number', round($value), $arg[2]];
980
    }
981
982
    protected function lib_unit($arg)
983
    {
984
        if ($arg[0] == 'list') {
985
            list($number, $newUnit) = $arg[2];
986
987
            return [
988
                'number',
989
                $this->assertNumber($number),
990
                $this->compileValue($this->lib_e($newUnit)),
991
            ];
992
        } else {
993
            return ['number', $this->assertNumber($arg), ''];
994
        }
995
    }
996
997
    /**
998
     * Helper function to get arguments for color manipulation functions.
999
     * takes a list that contains a color like thing and a percentage.
1000
     */
1001
    protected function colorArgs($args)
1002
    {
1003 View Code Duplication
        if ($args[0] != 'list' || count($args[2]) < 2) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1004
            return [['color', 0, 0, 0], 0];
1005
        }
1006
        list($color, $delta) = $args[2];
1007
        $color = $this->assertColor($color);
1008
        $delta = floatval($delta[1]);
1009
1010
        return [$color, $delta];
1011
    }
1012
1013 View Code Duplication
    protected function lib_darken($args)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1014
    {
1015
        list($color, $delta) = $this->colorArgs($args);
1016
1017
        $hsl = $this->toHSL($color);
1018
        $hsl[3] = $this->clamp($hsl[3] - $delta, 100);
1019
1020
        return $this->toRGB($hsl);
1021
    }
1022
1023 View Code Duplication
    protected function lib_lighten($args)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1024
    {
1025
        list($color, $delta) = $this->colorArgs($args);
1026
1027
        $hsl = $this->toHSL($color);
1028
        $hsl[3] = $this->clamp($hsl[3] + $delta, 100);
1029
1030
        return $this->toRGB($hsl);
1031
    }
1032
1033 View Code Duplication
    protected function lib_saturate($args)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1034
    {
1035
        list($color, $delta) = $this->colorArgs($args);
1036
1037
        $hsl = $this->toHSL($color);
1038
        $hsl[2] = $this->clamp($hsl[2] + $delta, 100);
1039
1040
        return $this->toRGB($hsl);
1041
    }
1042
1043 View Code Duplication
    protected function lib_desaturate($args)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1044
    {
1045
        list($color, $delta) = $this->colorArgs($args);
1046
1047
        $hsl = $this->toHSL($color);
1048
        $hsl[2] = $this->clamp($hsl[2] - $delta, 100);
1049
1050
        return $this->toRGB($hsl);
1051
    }
1052
1053
    protected function lib_spin($args)
1054
    {
1055
        list($color, $delta) = $this->colorArgs($args);
1056
1057
        $hsl = $this->toHSL($color);
1058
1059
        $hsl[1] = $hsl[1] + $delta % 360;
1060
        if ($hsl[1] < 0) {
1061
            $hsl[1] += 360;
1062
        }
1063
1064
        return $this->toRGB($hsl);
1065
    }
1066
1067 View Code Duplication
    protected function lib_fadeout($args)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1068
    {
1069
        list($color, $delta) = $this->colorArgs($args);
1070
        $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta / 100);
1071
1072
        return $color;
1073
    }
1074
1075 View Code Duplication
    protected function lib_fadein($args)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1076
    {
1077
        list($color, $delta) = $this->colorArgs($args);
1078
        $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta / 100);
1079
1080
        return $color;
1081
    }
1082
1083
    protected function lib_hue($color)
1084
    {
1085
        $hsl = $this->toHSL($this->assertColor($color));
1086
1087
        return round($hsl[1]);
1088
    }
1089
1090
    protected function lib_saturation($color)
1091
    {
1092
        $hsl = $this->toHSL($this->assertColor($color));
1093
1094
        return round($hsl[2]);
1095
    }
1096
1097
    protected function lib_lightness($color)
1098
    {
1099
        $hsl = $this->toHSL($this->assertColor($color));
1100
1101
        return round($hsl[3]);
1102
    }
1103
1104
    // get the alpha of a color
1105
    // defaults to 1 for non-colors or colors without an alpha
1106
    protected function lib_alpha($value)
1107
    {
1108
        if (!is_null($color = $this->coerceColor($value))) {
1109
            return isset($color[4]) ? $color[4] : 1;
1110
        }
1111
    }
1112
1113
    // set the alpha of the color
1114
    protected function lib_fade($args)
1115
    {
1116
        list($color, $alpha) = $this->colorArgs($args);
1117
        $color[4] = $this->clamp($alpha / 100.0);
1118
1119
        return $color;
1120
    }
1121
1122
    protected function lib_percentage($arg)
1123
    {
1124
        $num = $this->assertNumber($arg);
1125
1126
        return ['number', $num * 100, '%'];
1127
    }
1128
1129
    // mixes two colors by weight
1130
    // mix(@color1, @color2, @weight);
1131
    // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method
1132
    protected function lib_mix($args)
1133
    {
1134
        if ($args[0] != 'list' || count($args[2]) < 3) {
1135
            $this->throwError('mix expects (color1, color2, weight)');
1136
        }
1137
1138
        list($first, $second, $weight) = $args[2];
1139
        $first = $this->assertColor($first);
1140
        $second = $this->assertColor($second);
1141
1142
        $first_a = $this->lib_alpha($first);
1143
        $second_a = $this->lib_alpha($second);
1144
        $weight = $weight[1] / 100.0;
1145
1146
        $w = $weight * 2 - 1;
1147
        $a = $first_a - $second_a;
1148
1149
        $w1 = (($w * $a == -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
1150
        $w2 = 1.0 - $w1;
1151
1152
        $new = [
1153
            'color',
1154
            $w1 * $first[1] + $w2 * $second[1],
1155
            $w1 * $first[2] + $w2 * $second[2],
1156
            $w1 * $first[3] + $w2 * $second[3],
1157
        ];
1158
1159
        if ($first_a != 1.0 || $second_a != 1.0) {
1160
            $new[] = $first_a * $weight + $second_a * ($weight - 1);
1161
        }
1162
1163
        return $this->fixColor($new);
1164
    }
1165
1166
    protected function lib_contrast($args)
1167
    {
1168 View Code Duplication
        if ($args[0] != 'list' || count($args[2]) < 3) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1169
            return [['color', 0, 0, 0], 0];
1170
        }
1171
1172
        list($inputColor, $darkColor, $lightColor) = $args[2];
1173
1174
        $inputColor = $this->assertColor($inputColor);
1175
        $darkColor = $this->assertColor($darkColor);
1176
        $lightColor = $this->assertColor($lightColor);
1177
        $hsl = $this->toHSL($inputColor);
1178
1179
        if ($hsl[3] > 50) {
1180
            return $darkColor;
1181
        }
1182
1183
        return $lightColor;
1184
    }
1185
1186
    protected function assertColor($value, $error = 'expected color value')
1187
    {
1188
        $color = $this->coerceColor($value);
1189
        if (is_null($color)) {
1190
            $this->throwError($error);
1191
        }
1192
1193
        return $color;
1194
    }
1195
1196
    protected function assertNumber($value, $error = 'expecting number')
1197
    {
1198
        if ($value[0] == 'number') {
1199
            return $value[1];
1200
        }
1201
        $this->throwError($error);
1202
    }
1203
1204
    protected function toHSL($color)
1205
    {
1206
        if ($color[0] == 'hsl') {
1207
            return $color;
1208
        }
1209
1210
        $r = $color[1] / 255;
1211
        $g = $color[2] / 255;
1212
        $b = $color[3] / 255;
1213
1214
        $min = min($r, $g, $b);
1215
        $max = max($r, $g, $b);
1216
1217
        $L = ($min + $max) / 2;
1218
        if ($min == $max) {
1219
            $S = $H = 0;
1220
        } else {
1221 View Code Duplication
            if ($L < 0.5) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1222
                $S = ($max - $min) / ($max + $min);
1223
            } else {
1224
                $S = ($max - $min) / (2.0 - $max - $min);
1225
            }
1226
1227
            if ($r == $max) {
1228
                $H = ($g - $b) / ($max - $min);
1229
            } elseif ($g == $max) {
1230
                $H = 2.0 + ($b - $r) / ($max - $min);
1231
            } elseif ($b == $max) {
1232
                $H = 4.0 + ($r - $g) / ($max - $min);
1233
            }
1234
        }
1235
1236
        $out = [
1237
            'hsl',
1238
            ($H < 0 ? $H + 6 : $H) * 60,
0 ignored issues
show
Bug introduced by
The variable $H does not seem to be defined for all execution paths leading up to this point.

If you define a variable conditionally, it can happen that it is not defined for all execution paths.

Let’s take a look at an example:

function myFunction($a) {
    switch ($a) {
        case 'foo':
            $x = 1;
            break;

        case 'bar':
            $x = 2;
            break;
    }

    // $x is potentially undefined here.
    echo $x;
}

In the above example, the variable $x is defined if you pass “foo” or “bar” as argument for $a. However, since the switch statement has no default case statement, if you pass any other value, the variable $x would be undefined.

Available Fixes

  1. Check for existence of the variable explicitly:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        if (isset($x)) { // Make sure it's always set.
            echo $x;
        }
    }
    
  2. Define a default value for the variable:

    function myFunction($a) {
        $x = ''; // Set a default which gets overridden for certain paths.
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
        }
    
        echo $x;
    }
    
  3. Add a value for the missing path:

    function myFunction($a) {
        switch ($a) {
            case 'foo':
                $x = 1;
                break;
    
            case 'bar':
                $x = 2;
                break;
    
            // We add support for the missing case.
            default:
                $x = '';
                break;
        }
    
        echo $x;
    }
    
Loading history...
1239
            $S * 100,
1240
            $L * 100,
1241
        ];
1242
1243
        if (count($color) > 4) {
1244
            $out[] = $color[4];
1245
        } // copy alpha
1246
        return $out;
1247
    }
1248
1249
    protected function toRGB_helper($comp, $temp1, $temp2)
1250
    {
1251
        if ($comp < 0) {
1252
            $comp += 1.0;
1253
        } elseif ($comp > 1) {
1254
            $comp -= 1.0;
1255
        }
1256
1257 View Code Duplication
        if (6 * $comp < 1) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1258
            return $temp1 + ($temp2 - $temp1) * 6 * $comp;
1259
        }
1260
        if (2 * $comp < 1) {
1261
            return $temp2;
1262
        }
1263 View Code Duplication
        if (3 * $comp < 2) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1264
            return $temp1 + ($temp2 - $temp1) * ((2 / 3) - $comp) * 6;
1265
        }
1266
1267
        return $temp1;
1268
    }
1269
1270
    /**
1271
     * Converts a hsl array into a color value in rgb.
1272
     * Expects H to be in range of 0 to 360, S and L in 0 to 100.
1273
     */
1274
    protected function toRGB($color)
1275
    {
1276
        if ($color[0] == 'color') {
1277
            return $color;
1278
        }
1279
1280
        $H = $color[1] / 360;
1281
        $S = $color[2] / 100;
1282
        $L = $color[3] / 100;
1283
1284
        if ($S == 0) {
1285
            $r = $g = $b = $L;
1286
        } else {
1287
            $temp2 = $L < 0.5 ?
1288
                $L * (1.0 + $S) :
1289
                $L + $S - $L * $S;
1290
1291
            $temp1 = 2.0 * $L - $temp2;
1292
1293
            $r = $this->toRGB_helper($H + 1 / 3, $temp1, $temp2);
1294
            $g = $this->toRGB_helper($H, $temp1, $temp2);
1295
            $b = $this->toRGB_helper($H - 1 / 3, $temp1, $temp2);
1296
        }
1297
1298
        // $out = array('color', round($r*255), round($g*255), round($b*255));
1299
        $out = ['color', $r * 255, $g * 255, $b * 255];
1300
        if (count($color) > 4) {
1301
            $out[] = $color[4];
1302
        } // copy alpha
1303
        return $out;
1304
    }
1305
1306
    protected function clamp($v, $max = 1, $min = 0)
1307
    {
1308
        return min($max, max($min, $v));
1309
    }
1310
1311
    /**
1312
     * Convert the rgb, rgba, hsl color literals of function type
1313
     * as returned by the parser into values of color type.
1314
     */
1315
    protected function funcToColor($func)
1316
    {
1317
        $fname = $func[1];
1318
        if ($func[2][0] != 'list') {
1319
            return false;
1320
        } // need a list of arguments
1321
        $rawComponents = $func[2][2];
1322
1323
        if ($fname == 'hsl' || $fname == 'hsla') {
1324
            $hsl = ['hsl'];
1325
            $i = 0;
1326
            foreach ($rawComponents as $c) {
1327
                $val = $this->reduce($c);
1328
                $val = isset($val[1]) ? floatval($val[1]) : 0;
1329
1330
                if ($i == 0) {
1331
                    $clamp = 360;
1332
                } elseif ($i < 3) {
1333
                    $clamp = 100;
1334
                } else {
1335
                    $clamp = 1;
1336
                }
1337
1338
                $hsl[] = $this->clamp($val, $clamp);
1339
                $i++;
1340
            }
1341
1342
            while (count($hsl) < 4) {
1343
                $hsl[] = 0;
1344
            }
1345
1346
            return $this->toRGB($hsl);
1347
        } elseif ($fname == 'rgb' || $fname == 'rgba') {
1348
            $components = [];
1349
            $i = 1;
1350
            foreach ($rawComponents as $c) {
1351
                $c = $this->reduce($c);
1352
                if ($i < 4) {
1353 View Code Duplication
                    if ($c[0] == 'number' && $c[2] == '%') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1354
                        $components[] = 255 * ($c[1] / 100);
1355
                    } else {
1356
                        $components[] = floatval($c[1]);
1357
                    }
1358 View Code Duplication
                } elseif ($i == 4) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1359
                    if ($c[0] == 'number' && $c[2] == '%') {
1360
                        $components[] = 1.0 * ($c[1] / 100);
1361
                    } else {
1362
                        $components[] = floatval($c[1]);
1363
                    }
1364
                } else {
1365
                    break;
1366
                }
1367
1368
                $i++;
1369
            }
1370
            while (count($components) < 3) {
1371
                $components[] = 0;
1372
            }
1373
            array_unshift($components, 'color');
1374
1375
            return $this->fixColor($components);
1376
        }
1377
1378
        return false;
1379
    }
1380
1381
    protected function reduce($value, $forExpression = false)
1382
    {
1383
        switch ($value[0]) {
1384
            case 'interpolate':
1385
                $reduced = $this->reduce($value[1]);
1386
                $var = $this->compileValue($reduced);
1387
                $res = $this->reduce(['variable', $this->vPrefix.$var]);
1388
1389
                if (empty($value[2])) {
1390
                    $res = $this->lib_e($res);
1391
                }
1392
1393
                return $res;
1394
            case 'variable':
1395
                $key = $value[1];
1396
                if (is_array($key)) {
1397
                    $key = $this->reduce($key);
1398
                    $key = $this->vPrefix.$this->compileValue($this->lib_e($key));
1399
                }
1400
1401
                $seen = &$this->env->seenNames;
1402
1403
                if (!empty($seen[$key])) {
1404
                    $this->throwError("infinite loop detected: $key");
1405
                }
1406
1407
                $seen[$key] = true;
1408
                $out = $this->reduce($this->get($key, self::$defaultValue));
1409
                $seen[$key] = false;
1410
1411
                return $out;
1412
            case 'list':
1413
                foreach ($value[2] as &$item) {
1414
                    $item = $this->reduce($item, $forExpression);
1415
                }
1416
1417
                return $value;
1418
            case 'expression':
1419
                return $this->evaluate($value);
1420
            case 'string':
1421
                foreach ($value[2] as &$part) {
1422
                    if (is_array($part)) {
1423
                        $strip = $part[0] == 'variable';
1424
                        $part = $this->reduce($part);
1425
                        if ($strip) {
1426
                            $part = $this->lib_e($part);
1427
                        }
1428
                    }
1429
                }
1430
1431
                return $value;
1432
            case 'escape':
1433
                list(, $inner) = $value;
1434
1435
                return $this->lib_e($this->reduce($inner));
1436
            case 'function':
1437
                $color = $this->funcToColor($value);
1438
                if ($color) {
1439
                    return $color;
1440
                }
1441
1442
                list(, $name, $args) = $value;
1443
                if ($name == '%') {
1444
                    $name = '_sprintf';
1445
                }
1446
                $f = isset($this->libFunctions[$name]) ?
1447
                    $this->libFunctions[$name] : [$this, 'lib_'.$name];
1448
1449
                if (is_callable($f)) {
1450
                    if ($args[0] == 'list') {
1451
                        $args = self::compressList($args[2], $args[1]);
1452
                    }
1453
1454
                    $ret = call_user_func($f, $this->reduce($args, true), $this);
1455
1456
                    if (is_null($ret)) {
1457
                        return [
1458
                            'string',
1459
                            '',
1460
                            [
1461
                                $name,
1462
                                '(',
1463
                                $args,
1464
                                ')',
1465
                            ],
1466
                        ];
1467
                    }
1468
1469
                    // convert to a typed value if the result is a php primitive
1470
                    if (is_numeric($ret)) {
1471
                        $ret = ['number', $ret, ''];
1472
                    } elseif (!is_array($ret)) {
1473
                        $ret = ['keyword', $ret];
1474
                    }
1475
1476
                    return $ret;
1477
                }
1478
1479
                // plain function, reduce args
1480
                $value[2] = $this->reduce($value[2]);
1481
1482
                return $value;
1483
            case 'unary':
1484
                list(, $op, $exp) = $value;
1485
                $exp = $this->reduce($exp);
1486
1487
                if ($exp[0] == 'number') {
1488
                    switch ($op) {
1489
                        case '+':
1490
                            return $exp;
1491
                        case '-':
1492
                            $exp[1] *= -1;
1493
1494
                            return $exp;
1495
                    }
1496
                }
1497
1498
                return ['string', '', [$op, $exp]];
1499
        }
1500
1501
        if ($forExpression) {
1502
            switch ($value[0]) {
1503
                case 'keyword':
1504
                    if ($color = $this->coerceColor($value)) {
1505
                        return $color;
1506
                    }
1507
                    break;
1508
                case 'raw_color':
1509
                    return $this->coerceColor($value);
1510
            }
1511
        }
1512
1513
        return $value;
1514
    }
1515
1516
    // coerce a value for use in color operation
1517
    protected function coerceColor($value)
1518
    {
1519
        switch ($value[0]) {
1520
            case 'color':
1521
                return $value;
1522
            case 'raw_color':
1523
                $c = ['color', 0, 0, 0];
1524
                $colorStr = substr($value[1], 1);
1525
                $num = hexdec($colorStr);
1526
                $width = strlen($colorStr) == 3 ? 16 : 256;
1527
1528
                for ($i = 3; $i > 0; $i--) { // 3 2 1
1529
                    $t = $num % $width;
1530
                    $num /= $width;
1531
1532
                    $c[$i] = $t * (256 / $width) + $t * floor(16 / $width);
1533
                }
1534
1535
                return $c;
1536
            case 'keyword':
1537
                $name = $value[1];
1538
                if (isset(self::$cssColors[$name])) {
1539
                    $rgba = explode(',', self::$cssColors[$name]);
1540
1541
                    if (isset($rgba[3])) {
1542
                        return ['color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]];
1543
                    }
1544
1545
                    return ['color', $rgba[0], $rgba[1], $rgba[2]];
1546
                }
1547
1548
                return;
1549
        }
1550
    }
1551
1552
    // make something string like into a string
1553
    protected function coerceString($value)
1554
    {
1555
        switch ($value[0]) {
1556
            case 'string':
1557
                return $value;
1558
            case 'keyword':
1559
                return ['string', '', [$value[1]]];
1560
        }
1561
    }
1562
1563
    // turn list of length 1 into value type
1564
    protected function flattenList($value)
1565
    {
1566
        if ($value[0] == 'list' && count($value[2]) == 1) {
1567
            return $this->flattenList($value[2][0]);
1568
        }
1569
1570
        return $value;
1571
    }
1572
1573
    protected function toBool($a)
1574
    {
1575
        if ($a) {
1576
            return self::$TRUE;
1577
        } else {
1578
            return self::$FALSE;
1579
        }
1580
    }
1581
1582
    // evaluate an expression
1583
    protected function evaluate($exp)
1584
    {
1585
        list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
1586
1587
        $left = $this->reduce($left, true);
1588
        $right = $this->reduce($right, true);
1589
1590
        if ($leftColor = $this->coerceColor($left)) {
1591
            $left = $leftColor;
1592
        }
1593
1594
        if ($rightColor = $this->coerceColor($right)) {
1595
            $right = $rightColor;
1596
        }
1597
1598
        $ltype = $left[0];
1599
        $rtype = $right[0];
1600
1601
        // operators that work on all types
1602
        if ($op == 'and') {
1603
            return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
1604
        }
1605
1606
        if ($op == '=') {
1607
            return $this->toBool($this->eq($left, $right));
1608
        }
1609
1610
        if ($op == '+' && !is_null($str = $this->stringConcatenate($left, $right))) {
1611
            return $str;
1612
        }
1613
1614
        // type based operators
1615
        $fname = "op_${ltype}_${rtype}";
1616
        if (is_callable([$this, $fname])) {
1617
            $out = $this->$fname($op, $left, $right);
1618
            if (!is_null($out)) {
1619
                return $out;
1620
            }
1621
        }
1622
1623
        // make the expression look it did before being parsed
1624
        $paddedOp = $op;
1625
        if ($whiteBefore) {
1626
            $paddedOp = ' '.$paddedOp;
1627
        }
1628
        if ($whiteAfter) {
1629
            $paddedOp .= ' ';
1630
        }
1631
1632
        return ['string', '', [$left, $paddedOp, $right]];
1633
    }
1634
1635
    protected function stringConcatenate($left, $right)
1636
    {
1637
        if ($strLeft = $this->coerceString($left)) {
1638
            if ($right[0] == 'string') {
1639
                $right[1] = '';
1640
            }
1641
            $strLeft[2][] = $right;
1642
1643
            return $strLeft;
1644
        }
1645
1646
        if ($strRight = $this->coerceString($right)) {
1647
            array_unshift($strRight[2], $left);
1648
1649
            return $strRight;
1650
        }
1651
    }
1652
1653
    // make sure a color's components don't go out of bounds
1654
    protected function fixColor($c)
1655
    {
1656
        foreach (range(1, 3) as $i) {
1657
            if ($c[$i] < 0) {
1658
                $c[$i] = 0;
1659
            }
1660
            if ($c[$i] > 255) {
1661
                $c[$i] = 255;
1662
            }
1663
        }
1664
1665
        return $c;
1666
    }
1667
1668
    protected function op_number_color($op, $lft, $rgt)
1669
    {
1670
        if ($op == '+' || $op == '*') {
1671
            return $this->op_color_number($op, $rgt, $lft);
1672
        }
1673
    }
1674
1675
    protected function op_color_number($op, $lft, $rgt)
1676
    {
1677
        if ($rgt[0] == '%') {
1678
            $rgt[1] /= 100;
1679
        }
1680
1681
        return $this->op_color_color($op, $lft,
1682
            array_fill(1, count($lft) - 1, $rgt[1]));
1683
    }
1684
1685
    protected function op_color_color($op, $left, $right)
1686
    {
1687
        $out = ['color'];
1688
        $max = count($left) > count($right) ? count($left) : count($right);
1689
        foreach (range(1, $max - 1) as $i) {
1690
            $lval = isset($left[$i]) ? $left[$i] : 0;
1691
            $rval = isset($right[$i]) ? $right[$i] : 0;
1692
            switch ($op) {
1693
                case '+':
1694
                    $out[] = $lval + $rval;
1695
                    break;
1696
                case '-':
1697
                    $out[] = $lval - $rval;
1698
                    break;
1699
                case '*':
1700
                    $out[] = $lval * $rval;
1701
                    break;
1702
                case '%':
1703
                    $out[] = $lval % $rval;
1704
                    break;
1705
                case '/':
1706
                    if ($rval == 0) {
1707
                        $this->throwError("evaluate error: can't divide by zero");
1708
                    }
1709
                    $out[] = $lval / $rval;
1710
                    break;
1711
                default:
1712
                    $this->throwError('evaluate error: color op number failed on op '.$op);
1713
            }
1714
        }
1715
1716
        return $this->fixColor($out);
1717
    }
1718
1719 View Code Duplication
    public function lib_red($color)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1720
    {
1721
        $color = $this->coerceColor($color);
1722
        if (is_null($color)) {
1723
            $this->throwError('color expected for red()');
1724
        }
1725
1726
        return $color[1];
1727
    }
1728
1729 View Code Duplication
    public function lib_green($color)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1730
    {
1731
        $color = $this->coerceColor($color);
1732
        if (is_null($color)) {
1733
            $this->throwError('color expected for green()');
1734
        }
1735
1736
        return $color[2];
1737
    }
1738
1739 View Code Duplication
    public function lib_blue($color)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1740
    {
1741
        $color = $this->coerceColor($color);
1742
        if (is_null($color)) {
1743
            $this->throwError('color expected for blue()');
1744
        }
1745
1746
        return $color[3];
1747
    }
1748
1749
    // operator on two numbers
1750
    protected function op_number_number($op, $left, $right)
1751
    {
1752
        $unit = empty($left[2]) ? $right[2] : $left[2];
1753
1754
        $value = 0;
1755
        switch ($op) {
1756
            case '+':
1757
                $value = $left[1] + $right[1];
1758
                break;
1759
            case '*':
1760
                $value = $left[1] * $right[1];
1761
                break;
1762
            case '-':
1763
                $value = $left[1] - $right[1];
1764
                break;
1765
            case '%':
1766
                $value = $left[1] % $right[1];
1767
                break;
1768
            case '/':
1769
                if ($right[1] == 0) {
1770
                    $this->throwError('parse error: divide by zero');
1771
                }
1772
                $value = $left[1] / $right[1];
1773
                break;
1774
            case '<':
1775
                return $this->toBool($left[1] < $right[1]);
1776
            case '>':
1777
                return $this->toBool($left[1] > $right[1]);
1778
            case '>=':
1779
                return $this->toBool($left[1] >= $right[1]);
1780
            case '=<':
1781
                return $this->toBool($left[1] <= $right[1]);
1782
            default:
1783
                $this->throwError('parse error: unknown number operator: '.$op);
1784
        }
1785
1786
        return ['number', $value, $unit];
1787
    }
1788
1789
    /* environment functions */
1790
1791
    protected function makeOutputBlock($type, $selectors = null)
1792
    {
1793
        $b = new stdclass();
1794
        $b->lines = [];
1795
        $b->children = [];
1796
        $b->selectors = $selectors;
1797
        $b->type = $type;
1798
        $b->parent = $this->scope;
1799
1800
        return $b;
1801
    }
1802
1803
    // the state of execution
1804
    protected function pushEnv($block = null)
1805
    {
1806
        $e = new stdclass();
1807
        $e->parent = $this->env;
1808
        $e->store = [];
1809
        $e->block = $block;
1810
1811
        $this->env = $e;
1812
1813
        return $e;
1814
    }
1815
1816
    // pop something off the stack
1817
    protected function popEnv()
1818
    {
1819
        $old = $this->env;
1820
        $this->env = $this->env->parent;
1821
1822
        return $old;
1823
    }
1824
1825
    // set something in the current env
1826
    protected function set($name, $value)
1827
    {
1828
        $this->env->store[$name] = $value;
1829
    }
1830
1831
    // get the highest occurrence entry for a name
1832
    protected function get($name, $default = null)
1833
    {
1834
        $current = $this->env;
1835
1836
        $isArguments = $name == $this->vPrefix.'arguments';
1837
        while ($current) {
1838
            if ($isArguments && isset($current->arguments)) {
1839
                return ['list', ' ', $current->arguments];
1840
            }
1841
1842
            if (isset($current->store[$name])) {
1843
                return $current->store[$name];
1844
            } else {
1845
                $current = isset($current->storeParent) ?
1846
                    $current->storeParent : $current->parent;
1847
            }
1848
        }
1849
1850
        return $default;
1851
    }
1852
1853
    // inject array of unparsed strings into environment as variables
1854
    protected function injectVariables($args)
1855
    {
1856
        $this->pushEnv();
1857
        $parser = new lessc_parser($this, __METHOD__);
1858
        foreach ($args as $name => $strValue) {
1859
            if ($name[0] != '@') {
1860
                $name = '@'.$name;
1861
            }
1862
            $parser->count = 0;
0 ignored issues
show
Bug introduced by
The property count does not seem to exist in lessc_parser.

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

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

Loading history...
1863
            $parser->buffer = (string) $strValue;
0 ignored issues
show
Bug introduced by
The property buffer does not seem to exist in lessc_parser.

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

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

Loading history...
1864
            if (!$parser->propertyValue($value)) {
1865
                throw new Exception("failed to parse passed in variable $name: $strValue");
1866
            }
1867
1868
            $this->set($name, $value);
1869
        }
1870
    }
1871
1872
    /**
1873
     * Initialize any static state, can initialize parser for a file
1874
     * $opts isn't used yet.
1875
     */
1876
    public function __construct($fname = null)
1877
    {
1878
        if ($fname !== null) {
1879
            // used for deprecated parse method
1880
            $this->_parseFile = $fname;
0 ignored issues
show
Bug introduced by
The property _parseFile does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1881
        }
1882
    }
1883
1884
    public function compile($string, $name = null)
1885
    {
1886
        $locale = setlocale(LC_NUMERIC, 0);
1887
        setlocale(LC_NUMERIC, 'C');
1888
1889
        $this->parser = $this->makeParser($name);
0 ignored issues
show
Bug introduced by
The property parser does not seem to exist. Did you mean sourceParser?

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...
1890
        $root = $this->parser->parse($string);
0 ignored issues
show
Bug introduced by
The property parser does not seem to exist. Did you mean sourceParser?

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...
1891
1892
        $this->env = null;
1893
        $this->scope = null;
1894
1895
        $this->formatter = $this->newFormatter();
1896
1897
        if (!empty($this->registeredVars)) {
1898
            $this->injectVariables($this->registeredVars);
1899
        }
1900
1901
        $this->sourceParser = $this->parser; // used for error messages
0 ignored issues
show
Bug introduced by
The property parser does not seem to exist. Did you mean sourceParser?

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...
1902
        $this->compileBlock($root);
1903
1904
        ob_start();
1905
        $this->formatter->block($this->scope);
1906
        $out = ob_get_clean();
1907
        setlocale(LC_NUMERIC, $locale);
1908
1909
        return $out;
1910
    }
1911
1912
    public function compileFile($fname, $outFname = null)
1913
    {
1914
        if (!is_readable($fname)) {
1915
            throw new Exception('load error: failed to find '.$fname);
1916
        }
1917
1918
        $pi = pathinfo($fname);
1919
1920
        $oldImport = $this->importDir;
1921
1922
        $this->importDir = (array) $this->importDir;
0 ignored issues
show
Documentation Bug introduced by
It seems like (array) $this->importDir of type array is incompatible with the declared type string of property $importDir.

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...
1923
        $this->importDir[] = $pi['dirname'].'/';
1924
1925
        $this->allParsedFiles = [];
0 ignored issues
show
Bug introduced by
The property allParsedFiles does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
1926
        $this->addParsedFile($fname);
1927
1928
        $out = $this->compile(file_get_contents($fname), $fname);
1929
1930
        $this->importDir = $oldImport;
1931
1932
        if ($outFname !== null) {
1933
            return file_put_contents($outFname, $out);
1934
        }
1935
1936
        return $out;
1937
    }
1938
1939
    // compile only if changed input has changed or output doesn't exist
1940
    public function checkedCompile($in, $out)
1941
    {
1942
        if (!is_file($out) || filemtime($in) > filemtime($out)) {
1943
            $this->compileFile($in, $out);
1944
1945
            return true;
1946
        }
1947
1948
        return false;
1949
    }
1950
1951
    /**
1952
     * Execute lessphp on a .less file or a lessphp cache structure.
1953
     *
1954
     * The lessphp cache structure contains information about a specific
1955
     * less file having been parsed. It can be used as a hint for future
1956
     * calls to determine whether or not a rebuild is required.
1957
     *
1958
     * The cache structure contains two important keys that may be used
1959
     * externally:
1960
     *
1961
     * compiled: The final compiled CSS
1962
     * updated: The time (in seconds) the CSS was last compiled
1963
     *
1964
     * The cache structure is a plain-ol' PHP associative array and can
1965
     * be serialized and unserialized without a hitch.
1966
     *
1967
     * @param mixed $in    Input
1968
     * @param bool  $force Force rebuild?
1969
     *
1970
     * @return array lessphp cache structure
1971
     */
1972
    public function cachedCompile($in, $force = false)
1973
    {
1974
        // assume no root
1975
        $root = null;
1976
1977
        if (is_string($in)) {
1978
            $root = $in;
1979
        } elseif (is_array($in) and isset($in['root'])) {
1980
            if ($force or !isset($in['files'])) {
1981
                // If we are forcing a recompile or if for some reason the
1982
                // structure does not contain any file information we should
1983
                // specify the root to trigger a rebuild.
1984
                $root = $in['root'];
1985
            } elseif (isset($in['files']) and is_array($in['files'])) {
1986
                foreach ($in['files'] as $fname => $ftime) {
1987
                    if (!file_exists($fname) or filemtime($fname) > $ftime) {
1988
                        // One of the files we knew about previously has changed
1989
                        // so we should look at our incoming root again.
1990
                        $root = $in['root'];
1991
                        break;
1992
                    }
1993
                }
1994
            }
1995
        } else {
1996
            // TODO: Throw an exception? We got neither a string nor something
1997
            // that looks like a compatible lessphp cache structure.
1998
            return;
1999
        }
2000
2001
        if ($root !== null) {
2002
            // If we have a root value which means we should rebuild.
2003
            $out = [];
2004
            $out['root'] = $root;
2005
            $out['compiled'] = $this->compileFile($root);
2006
            $out['files'] = $this->allParsedFiles();
2007
            $out['updated'] = time();
2008
2009
            return $out;
2010
        } else {
2011
            // No changes, pass back the structure
2012
            // we were given initially.
2013
            return $in;
2014
        }
2015
    }
2016
2017
    // parse and compile buffer
2018
    // This is deprecated
2019
    public function parse($str = null, $initialVariables = null)
2020
    {
2021
        if (is_array($str)) {
2022
            $initialVariables = $str;
2023
            $str = null;
2024
        }
2025
2026
        $oldVars = $this->registeredVars;
2027
        if ($initialVariables !== null) {
2028
            $this->setVariables($initialVariables);
2029
        }
2030
2031
        if ($str == null) {
2032
            if (empty($this->_parseFile)) {
2033
                throw new exception('nothing to parse');
2034
            }
2035
2036
            $out = $this->compileFile($this->_parseFile);
2037
        } else {
2038
            $out = $this->compile($str);
2039
        }
2040
2041
        $this->registeredVars = $oldVars;
2042
2043
        return $out;
2044
    }
2045
2046
    protected function makeParser($name)
2047
    {
2048
        $parser = new lessc_parser($this, $name);
2049
        $parser->writeComments = $this->preserveComments;
0 ignored issues
show
Bug introduced by
The property writeComments does not seem to exist in lessc_parser.

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

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

Loading history...
2050
2051
        return $parser;
2052
    }
2053
2054
    public function setFormatter($name)
2055
    {
2056
        $this->formatterName = $name;
0 ignored issues
show
Bug introduced by
The property formatterName does not seem to exist. Did you mean formatter?

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...
2057
    }
2058
2059
    protected function newFormatter()
2060
    {
2061
        $className = 'lessc_formatter_lessjs';
2062
        if (!empty($this->formatterName)) {
0 ignored issues
show
Bug introduced by
The property formatterName does not seem to exist. Did you mean formatter?

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...
2063
            if (!is_string($this->formatterName)) {
0 ignored issues
show
Bug introduced by
The property formatterName does not seem to exist. Did you mean formatter?

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...
2064
                return $this->formatterName;
0 ignored issues
show
Bug introduced by
The property formatterName does not seem to exist. Did you mean formatter?

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...
2065
            }
2066
            $className = "lessc_formatter_$this->formatterName";
0 ignored issues
show
Bug introduced by
The property formatterName does not seem to exist. Did you mean formatter?

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...
2067
        }
2068
2069
        return new $className();
2070
    }
2071
2072
    public function setPreserveComments($preserve)
2073
    {
2074
        $this->preserveComments = $preserve;
2075
    }
2076
2077
    public function registerFunction($name, $func)
2078
    {
2079
        $this->libFunctions[$name] = $func;
2080
    }
2081
2082
    public function unregisterFunction($name)
2083
    {
2084
        unset($this->libFunctions[$name]);
2085
    }
2086
2087
    public function setVariables($variables)
2088
    {
2089
        $this->registeredVars = array_merge($this->registeredVars, $variables);
2090
    }
2091
2092
    public function unsetVariable($name)
2093
    {
2094
        unset($this->registeredVars[$name]);
2095
    }
2096
2097
    public function setImportDir($dirs)
2098
    {
2099
        $this->importDir = (array) $dirs;
0 ignored issues
show
Documentation Bug introduced by
It seems like (array) $dirs of type array is incompatible with the declared type string of property $importDir.

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...
2100
    }
2101
2102
    public function addImportDir($dir)
2103
    {
2104
        $this->importDir = (array) $this->importDir;
0 ignored issues
show
Documentation Bug introduced by
It seems like (array) $this->importDir of type array is incompatible with the declared type string of property $importDir.

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...
2105
        $this->importDir[] = $dir;
2106
    }
2107
2108
    public function allParsedFiles()
2109
    {
2110
        return $this->allParsedFiles;
2111
    }
2112
2113
    protected function addParsedFile($file)
2114
    {
2115
        $this->allParsedFiles[realpath($file)] = filemtime($file);
2116
    }
2117
2118
    /**
2119
     * Uses the current value of $this->count to show line and line number.
2120
     */
2121
    protected function throwError($msg = null)
2122
    {
2123
        if ($this->sourceLoc >= 0) {
2124
            $this->sourceParser->throwError($msg, $this->sourceLoc);
2125
        }
2126
        throw new exception($msg);
2127
    }
2128
2129
    // compile file $in to file $out if $in is newer than $out
2130
    // returns true when it compiles, false otherwise
2131
    public static function ccompile($in, $out, $less = null)
2132
    {
2133
        if ($less === null) {
2134
            $less = new self();
2135
        }
2136
2137
        return $less->checkedCompile($in, $out);
2138
    }
2139
2140
    public static function cexecute($in, $force = false, $less = null)
2141
    {
2142
        if ($less === null) {
2143
            $less = new self();
2144
        }
2145
2146
        return $less->cachedCompile($in, $force);
2147
    }
2148
2149
    protected static $cssColors = [
2150
        'aliceblue'            => '240,248,255',
2151
        'antiquewhite'         => '250,235,215',
2152
        'aqua'                 => '0,255,255',
2153
        'aquamarine'           => '127,255,212',
2154
        'azure'                => '240,255,255',
2155
        'beige'                => '245,245,220',
2156
        'bisque'               => '255,228,196',
2157
        'black'                => '0,0,0',
2158
        'blanchedalmond'       => '255,235,205',
2159
        'blue'                 => '0,0,255',
2160
        'blueviolet'           => '138,43,226',
2161
        'brown'                => '165,42,42',
2162
        'burlywood'            => '222,184,135',
2163
        'cadetblue'            => '95,158,160',
2164
        'chartreuse'           => '127,255,0',
2165
        'chocolate'            => '210,105,30',
2166
        'coral'                => '255,127,80',
2167
        'cornflowerblue'       => '100,149,237',
2168
        'cornsilk'             => '255,248,220',
2169
        'crimson'              => '220,20,60',
2170
        'cyan'                 => '0,255,255',
2171
        'darkblue'             => '0,0,139',
2172
        'darkcyan'             => '0,139,139',
2173
        'darkgoldenrod'        => '184,134,11',
2174
        'darkgray'             => '169,169,169',
2175
        'darkgreen'            => '0,100,0',
2176
        'darkgrey'             => '169,169,169',
2177
        'darkkhaki'            => '189,183,107',
2178
        'darkmagenta'          => '139,0,139',
2179
        'darkolivegreen'       => '85,107,47',
2180
        'darkorange'           => '255,140,0',
2181
        'darkorchid'           => '153,50,204',
2182
        'darkred'              => '139,0,0',
2183
        'darksalmon'           => '233,150,122',
2184
        'darkseagreen'         => '143,188,143',
2185
        'darkslateblue'        => '72,61,139',
2186
        'darkslategray'        => '47,79,79',
2187
        'darkslategrey'        => '47,79,79',
2188
        'darkturquoise'        => '0,206,209',
2189
        'darkviolet'           => '148,0,211',
2190
        'deeppink'             => '255,20,147',
2191
        'deepskyblue'          => '0,191,255',
2192
        'dimgray'              => '105,105,105',
2193
        'dimgrey'              => '105,105,105',
2194
        'dodgerblue'           => '30,144,255',
2195
        'firebrick'            => '178,34,34',
2196
        'floralwhite'          => '255,250,240',
2197
        'forestgreen'          => '34,139,34',
2198
        'fuchsia'              => '255,0,255',
2199
        'gainsboro'            => '220,220,220',
2200
        'ghostwhite'           => '248,248,255',
2201
        'gold'                 => '255,215,0',
2202
        'goldenrod'            => '218,165,32',
2203
        'gray'                 => '128,128,128',
2204
        'green'                => '0,128,0',
2205
        'greenyellow'          => '173,255,47',
2206
        'grey'                 => '128,128,128',
2207
        'honeydew'             => '240,255,240',
2208
        'hotpink'              => '255,105,180',
2209
        'indianred'            => '205,92,92',
2210
        'indigo'               => '75,0,130',
2211
        'ivory'                => '255,255,240',
2212
        'khaki'                => '240,230,140',
2213
        'lavender'             => '230,230,250',
2214
        'lavenderblush'        => '255,240,245',
2215
        'lawngreen'            => '124,252,0',
2216
        'lemonchiffon'         => '255,250,205',
2217
        'lightblue'            => '173,216,230',
2218
        'lightcoral'           => '240,128,128',
2219
        'lightcyan'            => '224,255,255',
2220
        'lightgoldenrodyellow' => '250,250,210',
2221
        'lightgray'            => '211,211,211',
2222
        'lightgreen'           => '144,238,144',
2223
        'lightgrey'            => '211,211,211',
2224
        'lightpink'            => '255,182,193',
2225
        'lightsalmon'          => '255,160,122',
2226
        'lightseagreen'        => '32,178,170',
2227
        'lightskyblue'         => '135,206,250',
2228
        'lightslategray'       => '119,136,153',
2229
        'lightslategrey'       => '119,136,153',
2230
        'lightsteelblue'       => '176,196,222',
2231
        'lightyellow'          => '255,255,224',
2232
        'lime'                 => '0,255,0',
2233
        'limegreen'            => '50,205,50',
2234
        'linen'                => '250,240,230',
2235
        'magenta'              => '255,0,255',
2236
        'maroon'               => '128,0,0',
2237
        'mediumaquamarine'     => '102,205,170',
2238
        'mediumblue'           => '0,0,205',
2239
        'mediumorchid'         => '186,85,211',
2240
        'mediumpurple'         => '147,112,219',
2241
        'mediumseagreen'       => '60,179,113',
2242
        'mediumslateblue'      => '123,104,238',
2243
        'mediumspringgreen'    => '0,250,154',
2244
        'mediumturquoise'      => '72,209,204',
2245
        'mediumvioletred'      => '199,21,133',
2246
        'midnightblue'         => '25,25,112',
2247
        'mintcream'            => '245,255,250',
2248
        'mistyrose'            => '255,228,225',
2249
        'moccasin'             => '255,228,181',
2250
        'navajowhite'          => '255,222,173',
2251
        'navy'                 => '0,0,128',
2252
        'oldlace'              => '253,245,230',
2253
        'olive'                => '128,128,0',
2254
        'olivedrab'            => '107,142,35',
2255
        'orange'               => '255,165,0',
2256
        'orangered'            => '255,69,0',
2257
        'orchid'               => '218,112,214',
2258
        'palegoldenrod'        => '238,232,170',
2259
        'palegreen'            => '152,251,152',
2260
        'paleturquoise'        => '175,238,238',
2261
        'palevioletred'        => '219,112,147',
2262
        'papayawhip'           => '255,239,213',
2263
        'peachpuff'            => '255,218,185',
2264
        'peru'                 => '205,133,63',
2265
        'pink'                 => '255,192,203',
2266
        'plum'                 => '221,160,221',
2267
        'powderblue'           => '176,224,230',
2268
        'purple'               => '128,0,128',
2269
        'red'                  => '255,0,0',
2270
        'rosybrown'            => '188,143,143',
2271
        'royalblue'            => '65,105,225',
2272
        'saddlebrown'          => '139,69,19',
2273
        'salmon'               => '250,128,114',
2274
        'sandybrown'           => '244,164,96',
2275
        'seagreen'             => '46,139,87',
2276
        'seashell'             => '255,245,238',
2277
        'sienna'               => '160,82,45',
2278
        'silver'               => '192,192,192',
2279
        'skyblue'              => '135,206,235',
2280
        'slateblue'            => '106,90,205',
2281
        'slategray'            => '112,128,144',
2282
        'slategrey'            => '112,128,144',
2283
        'snow'                 => '255,250,250',
2284
        'springgreen'          => '0,255,127',
2285
        'steelblue'            => '70,130,180',
2286
        'tan'                  => '210,180,140',
2287
        'teal'                 => '0,128,128',
2288
        'thistle'              => '216,191,216',
2289
        'tomato'               => '255,99,71',
2290
        'transparent'          => '0,0,0,0',
2291
        'turquoise'            => '64,224,208',
2292
        'violet'               => '238,130,238',
2293
        'wheat'                => '245,222,179',
2294
        'white'                => '255,255,255',
2295
        'whitesmoke'           => '245,245,245',
2296
        'yellow'               => '255,255,0',
2297
        'yellowgreen'          => '154,205,50',
2298
    ];
2299
}
2300
2301
// responsible for taking a string of LESS code and converting it into a
2302
// syntax tree
2303
class lessc_parser
2304
{
2305
    protected static $nextBlockId = 0; // used to uniquely identify blocks
2306
2307
    protected static $precedence = [
2308
        '=<' => 0,
2309
        '>=' => 0,
2310
        '='  => 0,
2311
        '<'  => 0,
2312
        '>'  => 0,
2313
        '+'  => 1,
2314
        '-'  => 1,
2315
        '*'  => 2,
2316
        '/'  => 2,
2317
        '%'  => 2,
2318
    ];
2319
2320
    protected static $whitePattern;
2321
    protected static $commentMulti;
2322
2323
    protected static $commentSingle = '//';
2324
    protected static $commentMultiLeft = '/*';
2325
    protected static $commentMultiRight = '*/';
2326
2327
    // regex string to match any of the operators
2328
    protected static $operatorString;
2329
2330
    // these properties will supress division unless it's inside parenthases
2331
    protected static $supressDivisionProps =
2332
        ['/border-radius$/i', '/^font$/i'];
2333
2334
    protected $blockDirectives = ['font-face', 'keyframes', 'page', '-moz-document'];
2335
    protected $lineDirectives = ['charset'];
2336
2337
    /**
2338
     * if we are in parens we can be more liberal with whitespace around
2339
     * operators because it must evaluate to a single value and thus is less
2340
     * ambiguous.
2341
     *
2342
     * Consider:
2343
     *     property1: 10 -5; // is two numbers, 10 and -5
2344
     *     property2: (10 -5); // should evaluate to 5
2345
     */
2346
    protected $inParens = false;
2347
2348
    // caches preg escaped literals
2349
    protected static $literalCache = [];
2350
2351
    public function __construct($lessc, $sourceName = null)
2352
    {
2353
        $this->eatWhiteDefault = true;
0 ignored issues
show
Bug introduced by
The property eatWhiteDefault does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2354
        // reference to less needed for vPrefix, mPrefix, and parentSelector
2355
        $this->lessc = $lessc;
0 ignored issues
show
Bug introduced by
The property lessc does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2356
2357
        $this->sourceName = $sourceName; // name used for error messages
0 ignored issues
show
Bug introduced by
The property sourceName does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2358
2359
        $this->writeComments = false;
0 ignored issues
show
Bug introduced by
The property writeComments does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2360
2361
        if (!self::$operatorString) {
2362
            self::$operatorString =
2363
                '('.implode('|', array_map(['lessc', 'preg_quote'],
2364
                    array_keys(self::$precedence))).')';
2365
2366
            $commentSingle = lessc::preg_quote(self::$commentSingle);
2367
            $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft);
2368
            $commentMultiRight = lessc::preg_quote(self::$commentMultiRight);
2369
2370
            self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
2371
            self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
2372
        }
2373
    }
2374
2375
    public function parse($buffer)
2376
    {
2377
        $this->count = 0;
0 ignored issues
show
Bug introduced by
The property count does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2378
        $this->line = 1;
0 ignored issues
show
Bug introduced by
The property line does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2379
2380
        $this->env = null; // block stack
0 ignored issues
show
Bug introduced by
The property env does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2381
        $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
0 ignored issues
show
Bug introduced by
The property buffer does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2382
        $this->pushSpecialBlock('root');
2383
        $this->eatWhiteDefault = true;
2384
        $this->seenComments = [];
0 ignored issues
show
Bug introduced by
The property seenComments does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2385
2386
        // trim whitespace on head
2387
        // if (preg_match('/^\s+/', $this->buffer, $m)) {
2388
        // 	$this->line += substr_count($m[0], "\n");
2389
        // 	$this->buffer = ltrim($this->buffer);
2390
        // }
2391
        $this->whitespace();
2392
2393
        // parse the entire file
2394
        $lastCount = $this->count;
0 ignored issues
show
Unused Code introduced by
$lastCount 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...
2395
        while (false !== $this->parseChunk()) {
2396
        }
2397
2398
        if ($this->count != strlen($this->buffer)) {
2399
            $this->throwError();
2400
        }
2401
2402
        // TODO report where the block was opened
2403
        if (!is_null($this->env->parent)) {
2404
            throw new exception('parse error: unclosed block');
2405
        }
2406
2407
        return $this->env;
2408
    }
2409
2410
    /**
2411
     * Parse a single chunk off the head of the buffer and append it to the
2412
     * current parse environment.
2413
     * Returns false when the buffer is empty, or when there is an error.
2414
     *
2415
     * This function is called repeatedly until the entire document is
2416
     * parsed.
2417
     *
2418
     * This parser is most similar to a recursive descent parser. Single
2419
     * functions represent discrete grammatical rules for the language, and
2420
     * they are able to capture the text that represents those rules.
2421
     *
2422
     * Consider the function lessc::keyword(). (all parse functions are
2423
     * structured the same)
2424
     *
2425
     * The function takes a single reference argument. When calling the
2426
     * function it will attempt to match a keyword on the head of the buffer.
2427
     * If it is successful, it will place the keyword in the referenced
2428
     * argument, advance the position in the buffer, and return true. If it
2429
     * fails then it won't advance the buffer and it will return false.
2430
     *
2431
     * All of these parse functions are powered by lessc::match(), which behaves
2432
     * the same way, but takes a literal regular expression. Sometimes it is
2433
     * more convenient to use match instead of creating a new function.
2434
     *
2435
     * Because of the format of the functions, to parse an entire string of
2436
     * grammatical rules, you can chain them together using &&.
2437
     *
2438
     * But, if some of the rules in the chain succeed before one fails, then
2439
     * the buffer position will be left at an invalid state. In order to
2440
     * avoid this, lessc::seek() is used to remember and set buffer positions.
2441
     *
2442
     * Before parsing a chain, use $s = $this->seek() to remember the current
2443
     * position into $s. Then if a chain fails, use $this->seek($s) to
2444
     * go back where we started.
2445
     */
2446
    protected function parseChunk()
2447
    {
2448
        if (empty($this->buffer)) {
2449
            return false;
2450
        }
2451
        $s = $this->seek();
2452
2453
        // setting a property
2454 View Code Duplication
        if ($this->keyword($key) && $this->assign() &&
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2455
            $this->propertyValue($value, $key) && $this->end()
2456
        ) {
2457
            $this->append(['assign', $key, $value], $s);
2458
2459
            return true;
2460
        } else {
2461
            $this->seek($s);
2462
        }
2463
2464
        // look for special css blocks
2465
        if ($this->literal('@', false)) {
2466
            $this->count--;
2467
2468
            // media
2469
            if ($this->literal('@media')) {
2470
                if (($this->mediaQueryList($mediaQueries) || true)
2471
                    && $this->literal('{')
2472
                ) {
2473
                    $media = $this->pushSpecialBlock('media');
2474
                    $media->queries = is_null($mediaQueries) ? [] : $mediaQueries;
2475
2476
                    return true;
2477
                } else {
2478
                    $this->seek($s);
2479
2480
                    return false;
2481
                }
2482
            }
2483
2484
            if ($this->literal('@', false) && $this->keyword($dirName)) {
2485
                if ($this->isDirective($dirName, $this->blockDirectives)) {
2486
                    if (($this->openString('{', $dirValue, null, [';']) || true) &&
2487
                        $this->literal('{')
2488
                    ) {
2489
                        $dir = $this->pushSpecialBlock('directive');
2490
                        $dir->name = $dirName;
2491
                        if (isset($dirValue)) {
2492
                            $dir->value = $dirValue;
2493
                        }
2494
2495
                        return true;
2496
                    }
2497
                } elseif ($this->isDirective($dirName, $this->lineDirectives)) {
2498
                    if ($this->propertyValue($dirValue) && $this->end()) {
2499
                        $this->append(['directive', $dirName, $dirValue]);
2500
2501
                        return true;
2502
                    }
2503
                }
2504
            }
2505
2506
            $this->seek($s);
2507
        }
2508
2509
        // setting a variable
2510 View Code Duplication
        if ($this->variable($var) && $this->assign() &&
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2511
            $this->propertyValue($value) && $this->end()
2512
        ) {
2513
            $this->append(['assign', $var, $value], $s);
2514
2515
            return true;
2516
        } else {
2517
            $this->seek($s);
2518
        }
2519
2520
        if ($this->import($importValue)) {
2521
            $this->append($importValue, $s);
2522
2523
            return true;
2524
        }
2525
2526
        // opening parametric mixin
2527
        if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
2528
            ($this->guards($guards) || true) &&
2529
            $this->literal('{')
2530
        ) {
2531
            $block = $this->pushBlock($this->fixTags([$tag]));
2532
            $block->args = $args;
2533
            $block->isVararg = $isVararg;
2534
            if (!empty($guards)) {
2535
                $block->guards = $guards;
2536
            }
2537
2538
            return true;
2539
        } else {
2540
            $this->seek($s);
2541
        }
2542
2543
        // opening a simple block
2544
        if ($this->tags($tags) && $this->literal('{')) {
2545
            $tags = $this->fixTags($tags);
2546
            $this->pushBlock($tags);
2547
2548
            return true;
2549
        } else {
2550
            $this->seek($s);
2551
        }
2552
2553
        // closing a block
2554
        if ($this->literal('}', false)) {
2555
            try {
2556
                $block = $this->pop();
2557
            } catch (exception $e) {
2558
                $this->seek($s);
2559
                $this->throwError($e->getMessage());
2560
            }
2561
2562
            $hidden = false;
2563
            if (is_null($block->type)) {
2564
                $hidden = true;
2565
                if (!isset($block->args)) {
2566
                    foreach ($block->tags as $tag) {
2567
                        if (!is_string($tag) || $tag[0] != $this->lessc->mPrefix) {
2568
                            $hidden = false;
2569
                            break;
2570
                        }
2571
                    }
2572
                }
2573
2574
                foreach ($block->tags as $tag) {
2575
                    if (is_string($tag)) {
2576
                        $this->env->children[$tag][] = $block;
2577
                    }
2578
                }
2579
            }
2580
2581
            if (!$hidden) {
2582
                $this->append(['block', $block], $s);
2583
            }
2584
2585
            // this is done here so comments aren't bundled into he block that
2586
            // was just closed
2587
            $this->whitespace();
2588
2589
            return true;
2590
        }
2591
2592
        // mixin
2593
        if ($this->mixinTags($tags) &&
2594
            ($this->argumentValues($argv) || true) &&
2595
            ($this->keyword($suffix) || true) && $this->end()
2596
        ) {
2597
            $tags = $this->fixTags($tags);
2598
            $this->append(['mixin', $tags, $argv, $suffix], $s);
2599
2600
            return true;
2601
        } else {
2602
            $this->seek($s);
2603
        }
2604
2605
        // spare ;
2606
        if ($this->literal(';')) {
2607
            return true;
2608
        }
2609
2610
        return false; // got nothing, throw error
2611
    }
2612
2613
    protected function isDirective($dirname, $directives)
2614
    {
2615
        // TODO: cache pattern in parser
2616
        $pattern = implode('|',
2617
            array_map(['lessc', 'preg_quote'], $directives));
2618
        $pattern = '/^(-[a-z-]+-)?('.$pattern.')$/i';
2619
2620
        return preg_match($pattern, $dirname);
2621
    }
2622
2623
    protected function fixTags($tags)
2624
    {
2625
        // move @ tags out of variable namespace
2626
        foreach ($tags as &$tag) {
2627
            if ($tag[0] == $this->lessc->vPrefix) {
2628
                $tag[0] = $this->lessc->mPrefix;
2629
            }
2630
        }
2631
2632
        return $tags;
2633
    }
2634
2635
    // a list of expressions
2636
    protected function expressionList(&$exps)
2637
    {
2638
        $values = [];
2639
2640
        while ($this->expression($exp)) {
2641
            $values[] = $exp;
2642
        }
2643
2644
        if (count($values) == 0) {
2645
            return false;
2646
        }
2647
2648
        $exps = lessc::compressList($values, ' ');
2649
2650
        return true;
2651
    }
2652
2653
    /**
2654
     * Attempt to consume an expression.
2655
     *
2656
     * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
2657
     */
2658
    protected function expression(&$out)
2659
    {
2660
        if ($this->value($lhs)) {
2661
            $out = $this->expHelper($lhs, 0);
2662
2663
            // look for / shorthand
2664
            if (!empty($this->env->supressedDivision)) {
2665
                unset($this->env->supressedDivision);
2666
                $s = $this->seek();
2667
                if ($this->literal('/') && $this->value($rhs)) {
2668
                    $out = [
2669
                        'list',
2670
                        '',
2671
                        [$out, ['keyword', '/'], $rhs],
2672
                    ];
2673
                } else {
2674
                    $this->seek($s);
2675
                }
2676
            }
2677
2678
            return true;
2679
        }
2680
2681
        return false;
2682
    }
2683
2684
    /**
2685
     * recursively parse infix equation with $lhs at precedence $minP.
2686
     */
2687
    protected function expHelper($lhs, $minP)
2688
    {
2689
        $this->inExp = true;
0 ignored issues
show
Bug introduced by
The property inExp does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
2690
        $ss = $this->seek();
2691
2692
        while (true) {
2693
            $whiteBefore = isset($this->buffer[$this->count - 1]) &&
2694
                ctype_space($this->buffer[$this->count - 1]);
2695
2696
            // If there is whitespace before the operator, then we require
2697
            // whitespace after the operator for it to be an expression
2698
            $needWhite = $whiteBefore && !$this->inParens;
2699
2700
            if ($this->match(self::$operatorString.($needWhite ? '\s' : ''),
2701
                    $m) && self::$precedence[$m[1]] >= $minP
2702
            ) {
2703
                if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == '/' && empty($this->env->supressedDivision)) {
2704
                    foreach (self::$supressDivisionProps as $pattern) {
2705
                        if (preg_match($pattern, $this->env->currentProperty)) {
2706
                            $this->env->supressedDivision = true;
2707
                            break 2;
2708
                        }
2709
                    }
2710
                }
2711
2712
                $whiteAfter = isset($this->buffer[$this->count - 1]) &&
2713
                    ctype_space($this->buffer[$this->count - 1]);
2714
2715
                if (!$this->value($rhs)) {
2716
                    break;
2717
                }
2718
2719
                // peek for next operator to see what to do with rhs
2720
                if ($this->peek(self::$operatorString,
2721
                        $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]
2722
                ) {
2723
                    $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
2724
                }
2725
2726
                $lhs = ['expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter];
2727
                $ss = $this->seek();
2728
2729
                continue;
2730
            }
2731
2732
            break;
2733
        }
2734
2735
        $this->seek($ss);
2736
2737
        return $lhs;
2738
    }
2739
2740
    // consume a list of values for a property
2741
    public function propertyValue(&$value, $keyName = null)
2742
    {
2743
        $values = [];
2744
2745
        if ($keyName !== null) {
2746
            $this->env->currentProperty = $keyName;
2747
        }
2748
2749
        $s = null;
2750
        while ($this->expressionList($v)) {
2751
            $values[] = $v;
2752
            $s = $this->seek();
2753
            if (!$this->literal(',')) {
2754
                break;
2755
            }
2756
        }
2757
2758
        if ($s) {
2759
            $this->seek($s);
2760
        }
2761
2762
        if ($keyName !== null) {
2763
            unset($this->env->currentProperty);
2764
        }
2765
2766
        if (count($values) == 0) {
2767
            return false;
2768
        }
2769
2770
        $value = lessc::compressList($values, ', ');
2771
2772
        return true;
2773
    }
2774
2775
    protected function parenValue(&$out)
2776
    {
2777
        $s = $this->seek();
2778
2779
        // speed shortcut
2780 View Code Duplication
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != '(') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
2781
            return false;
2782
        }
2783
2784
        $inParens = $this->inParens;
2785
        if ($this->literal('(') &&
2786
            ($this->inParens = true) && $this->expression($exp) &&
2787
            $this->literal(')')
2788
        ) {
2789
            $out = $exp;
2790
            $this->inParens = $inParens;
2791
2792
            return true;
2793
        } else {
2794
            $this->inParens = $inParens;
2795
            $this->seek($s);
2796
        }
2797
2798
        return false;
2799
    }
2800
2801
    // a single value
2802
    protected function value(&$value)
2803
    {
2804
        $s = $this->seek();
2805
2806
        // speed shortcut
2807
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == '-') {
2808
            // negation
2809
            if ($this->literal('-', false) &&
2810
                (($this->variable($inner) && $inner = ['variable', $inner]) ||
2811
                    $this->unit($inner) ||
2812
                    $this->parenValue($inner))
2813
            ) {
2814
                $value = ['unary', '-', $inner];
2815
2816
                return true;
2817
            } else {
2818
                $this->seek($s);
2819
            }
2820
        }
2821
2822
        if ($this->parenValue($value)) {
2823
            return true;
2824
        }
2825
        if ($this->unit($value)) {
2826
            return true;
2827
        }
2828
        if ($this->color($value)) {
2829
            return true;
2830
        }
2831
        if ($this->func($value)) {
2832
            return true;
2833
        }
2834
        if ($this->string($value)) {
2835
            return true;
2836
        }
2837
2838
        if ($this->keyword($word)) {
2839
            $value = ['keyword', $word];
2840
2841
            return true;
2842
        }
2843
2844
        // try a variable
2845
        if ($this->variable($var)) {
2846
            $value = ['variable', $var];
2847
2848
            return true;
2849
        }
2850
2851
        // unquote string (should this work on any type?
2852
        if ($this->literal('~') && $this->string($str)) {
2853
            $value = ['escape', $str];
2854
2855
            return true;
2856
        } else {
2857
            $this->seek($s);
2858
        }
2859
2860
        // css hack: \0
2861
        if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
2862
            $value = ['keyword', '\\'.$m[1]];
2863
2864
            return true;
2865
        } else {
2866
            $this->seek($s);
2867
        }
2868
2869
        return false;
2870
    }
2871
2872
    // an import statement
2873
    protected function import(&$out)
2874
    {
2875
        $s = $this->seek();
0 ignored issues
show
Unused Code introduced by
$s 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...
2876
        if (!$this->literal('@import')) {
2877
            return false;
2878
        }
2879
2880
        // @import "something.css" media;
2881
        // @import url("something.css") media;
2882
        // @import url(something.css) media;
2883
2884
        if ($this->propertyValue($value)) {
2885
            $out = ['import', $value];
2886
2887
            return true;
2888
        }
2889
    }
2890
2891
    protected function mediaQueryList(&$out)
2892
    {
2893
        if ($this->genericList($list, 'mediaQuery', ',', false)) {
2894
            $out = $list[2];
2895
2896
            return true;
2897
        }
2898
2899
        return false;
2900
    }
2901
2902
    protected function mediaQuery(&$out)
2903
    {
2904
        $s = $this->seek();
2905
2906
        $expressions = null;
2907
        $parts = [];
2908
2909
        if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) && $this->keyword($mediaType)) {
2910
            $prop = ['mediaType'];
2911
            if (isset($only)) {
2912
                $prop[] = 'only';
2913
            }
2914
            if (isset($not)) {
2915
                $prop[] = 'not';
2916
            }
2917
            $prop[] = $mediaType;
2918
            $parts[] = $prop;
2919
        } else {
2920
            $this->seek($s);
2921
        }
2922
2923
        if (!empty($mediaType) && !$this->literal('and')) {
2924
            // ~
2925
        } else {
2926
            $this->genericList($expressions, 'mediaExpression', 'and', false);
2927
            if (is_array($expressions)) {
2928
                $parts = array_merge($parts, $expressions[2]);
2929
            }
2930
        }
2931
2932
        if (count($parts) == 0) {
2933
            $this->seek($s);
2934
2935
            return false;
2936
        }
2937
2938
        $out = $parts;
2939
2940
        return true;
2941
    }
2942
2943
    protected function mediaExpression(&$out)
2944
    {
2945
        $s = $this->seek();
2946
        $value = null;
2947
        if ($this->literal('(') &&
2948
            $this->keyword($feature) &&
2949
            ($this->literal(':') && $this->expression($value) || true) &&
2950
            $this->literal(')')
2951
        ) {
2952
            $out = ['mediaExp', $feature];
2953
            if ($value) {
2954
                $out[] = $value;
2955
            }
2956
2957
            return true;
2958
        } elseif ($this->variable($variable)) {
2959
            $out = ['variable', $variable];
2960
2961
            return true;
2962
        }
2963
2964
        $this->seek($s);
2965
2966
        return false;
2967
    }
2968
2969
    // an unbounded string stopped by $end
2970
    protected function openString($end, &$out, $nestingOpen = null, $rejectStrs = null)
2971
    {
2972
        $oldWhite = $this->eatWhiteDefault;
2973
        $this->eatWhiteDefault = false;
2974
2975
        $stop = ["'", '"', '@{', $end];
2976
        $stop = array_map(['lessc', 'preg_quote'], $stop);
2977
        // $stop[] = self::$commentMulti;
2978
2979
        if (!is_null($rejectStrs)) {
2980
            $stop = array_merge($stop, $rejectStrs);
2981
        }
2982
2983
        $patt = '(.*?)('.implode('|', $stop).')';
2984
2985
        $nestingLevel = 0;
2986
2987
        $content = [];
2988
        while ($this->match($patt, $m, false)) {
2989
            if (!empty($m[1])) {
2990
                $content[] = $m[1];
2991
                if ($nestingOpen) {
2992
                    $nestingLevel += substr_count($m[1], $nestingOpen);
2993
                }
2994
            }
2995
2996
            $tok = $m[2];
2997
2998
            $this->count -= strlen($tok);
2999
            if ($tok == $end) {
3000
                if ($nestingLevel == 0) {
3001
                    break;
3002
                } else {
3003
                    $nestingLevel--;
3004
                }
3005
            }
3006
3007
            if (($tok == "'" || $tok == '"') && $this->string($str)) {
3008
                $content[] = $str;
3009
                continue;
3010
            }
3011
3012
            if ($tok == '@{' && $this->interpolation($inter)) {
3013
                $content[] = $inter;
3014
                continue;
3015
            }
3016
3017
            if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
3018
                $ount = null;
0 ignored issues
show
Unused Code introduced by
$ount 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...
3019
                break;
3020
            }
3021
3022
            $content[] = $tok;
3023
            $this->count += strlen($tok);
3024
        }
3025
3026
        $this->eatWhiteDefault = $oldWhite;
3027
3028
        if (count($content) == 0) {
3029
            return false;
3030
        }
3031
3032
        // trim the end
3033
        if (is_string(end($content))) {
3034
            $content[count($content) - 1] = rtrim(end($content));
3035
        }
3036
3037
        $out = ['string', '', $content];
3038
3039
        return true;
3040
    }
3041
3042
    protected function string(&$out)
3043
    {
3044
        $s = $this->seek();
3045
        if ($this->literal('"', false)) {
3046
            $delim = '"';
3047
        } elseif ($this->literal("'", false)) {
3048
            $delim = "'";
3049
        } else {
3050
            return false;
3051
        }
3052
3053
        $content = [];
3054
3055
        // look for either ending delim , escape, or string interpolation
3056
        $patt = '([^\n]*?)(@\{|\\\\|'.
3057
            lessc::preg_quote($delim).')';
3058
3059
        $oldWhite = $this->eatWhiteDefault;
3060
        $this->eatWhiteDefault = false;
3061
3062
        while ($this->match($patt, $m, false)) {
3063
            $content[] = $m[1];
3064
            if ($m[2] == '@{') {
3065
                $this->count -= strlen($m[2]);
3066
                if ($this->interpolation($inter, false)) {
0 ignored issues
show
Unused Code introduced by
The call to lessc_parser::interpolation() has too many arguments starting with false.

This check compares calls to functions or methods with their respective definitions. If the call has more arguments than are defined, it raises an issue.

If a function is defined several times with a different number of parameters, the check may pick up the wrong definition and report false positives. One codebase where this has been known to happen is Wordpress.

In this case you can add the @ignore PhpDoc annotation to the duplicate definition and it will be ignored.

Loading history...
3067
                    $content[] = $inter;
3068
                } else {
3069
                    $this->count += strlen($m[2]);
3070
                    $content[] = '@{'; // ignore it
3071
                }
3072
            } elseif ($m[2] == '\\') {
3073
                $content[] = $m[2];
3074
                if ($this->literal($delim, false)) {
3075
                    $content[] = $delim;
3076
                }
3077
            } else {
3078
                $this->count -= strlen($delim);
3079
                break; // delim
3080
            }
3081
        }
3082
3083
        $this->eatWhiteDefault = $oldWhite;
3084
3085
        if ($this->literal($delim)) {
3086
            $out = ['string', $delim, $content];
3087
3088
            return true;
3089
        }
3090
3091
        $this->seek($s);
3092
3093
        return false;
3094
    }
3095
3096
    protected function interpolation(&$out)
3097
    {
3098
        $oldWhite = $this->eatWhiteDefault;
3099
        $this->eatWhiteDefault = true;
3100
3101
        $s = $this->seek();
3102
        if ($this->literal('@{') &&
3103
            $this->openString('}', $interp, null, ["'", '"', ';']) &&
3104
            $this->literal('}', false)
3105
        ) {
3106
            $out = ['interpolate', $interp];
3107
            $this->eatWhiteDefault = $oldWhite;
3108
            if ($this->eatWhiteDefault) {
3109
                $this->whitespace();
3110
            }
3111
3112
            return true;
3113
        }
3114
3115
        $this->eatWhiteDefault = $oldWhite;
3116
        $this->seek($s);
3117
3118
        return false;
3119
    }
3120
3121
    protected function unit(&$unit)
3122
    {
3123
        // speed shortcut
3124
        if (isset($this->buffer[$this->count])) {
3125
            $char = $this->buffer[$this->count];
3126
            if (!ctype_digit($char) && $char != '.') {
3127
                return false;
3128
            }
3129
        }
3130
3131
        if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
3132
            $unit = ['number', $m[1], empty($m[2]) ? '' : $m[2]];
3133
3134
            return true;
3135
        }
3136
3137
        return false;
3138
    }
3139
3140
    // a # color
3141
    protected function color(&$out)
3142
    {
3143
        if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
3144
            if (strlen($m[1]) > 7) {
3145
                $out = ['string', '', [$m[1]]];
3146
            } else {
3147
                $out = ['raw_color', $m[1]];
3148
            }
3149
3150
            return true;
3151
        }
3152
3153
        return false;
3154
    }
3155
3156
    // consume a list of property values delimited by ; and wrapped in ()
3157
    protected function argumentValues(&$args, $delim = ',')
3158
    {
3159
        $s = $this->seek();
3160
        if (!$this->literal('(')) {
3161
            return false;
3162
        }
3163
3164
        $values = [];
3165
        while (true) {
3166
            if ($this->expressionList($value)) {
3167
                $values[] = $value;
3168
            }
3169
            if (!$this->literal($delim)) {
3170
                break;
3171
            } else {
3172
                if ($value == null) {
3173
                    $values[] = null;
3174
                }
3175
                $value = null;
3176
            }
3177
        }
3178
3179
        if (!$this->literal(')')) {
3180
            $this->seek($s);
3181
3182
            return false;
3183
        }
3184
3185
        $args = $values;
3186
3187
        return true;
3188
    }
3189
3190
    // consume an argument definition list surrounded by ()
3191
    // each argument is a variable name with optional value
3192
    // or at the end a ... or a variable named followed by ...
3193
    protected function argumentDef(&$args, &$isVararg, $delim = ',')
3194
    {
3195
        $s = $this->seek();
3196
        if (!$this->literal('(')) {
3197
            return false;
3198
        }
3199
3200
        $values = [];
3201
3202
        $isVararg = false;
3203
        while (true) {
3204
            if ($this->literal('...')) {
3205
                $isVararg = true;
3206
                break;
3207
            }
3208
3209
            if ($this->variable($vname)) {
3210
                $arg = ['arg', $vname];
3211
                $ss = $this->seek();
3212
                if ($this->assign() && $this->expressionList($value)) {
3213
                    $arg[] = $value;
3214
                } else {
3215
                    $this->seek($ss);
3216
                    if ($this->literal('...')) {
3217
                        $arg[0] = 'rest';
3218
                        $isVararg = true;
3219
                    }
3220
                }
3221
                $values[] = $arg;
3222
                if ($isVararg) {
3223
                    break;
3224
                }
3225
                continue;
3226
            }
3227
3228
            if ($this->value($literal)) {
3229
                $values[] = ['lit', $literal];
3230
            }
3231
3232
            if (!$this->literal($delim)) {
3233
                break;
3234
            }
3235
        }
3236
3237
        if (!$this->literal(')')) {
3238
            $this->seek($s);
3239
3240
            return false;
3241
        }
3242
3243
        $args = $values;
3244
3245
        return true;
3246
    }
3247
3248
    // consume a list of tags
3249
    // this accepts a hanging delimiter
3250 View Code Duplication
    protected function tags(&$tags, $simple = false, $delim = ',')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3251
    {
3252
        $tags = [];
3253
        while ($this->tag($tt, $simple)) {
3254
            $tags[] = $tt;
3255
            if (!$this->literal($delim)) {
3256
                break;
3257
            }
3258
        }
3259
        if (count($tags) == 0) {
3260
            return false;
3261
        }
3262
3263
        return true;
3264
    }
3265
3266
    // list of tags of specifying mixin path
3267
    // optionally separated by > (lazy, accepts extra >)
3268 View Code Duplication
    protected function mixinTags(&$tags)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3269
    {
3270
        $s = $this->seek();
0 ignored issues
show
Unused Code introduced by
$s 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...
3271
        $tags = [];
3272
        while ($this->tag($tt, true)) {
3273
            $tags[] = $tt;
3274
            $this->literal('>');
3275
        }
3276
3277
        if (count($tags) == 0) {
3278
            return false;
3279
        }
3280
3281
        return true;
3282
    }
3283
3284
    // a bracketed value (contained within in a tag definition)
3285
    protected function tagBracket(&$value)
3286
    {
3287
        // speed shortcut
3288 View Code Duplication
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != '[') {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3289
            return false;
3290
        }
3291
3292
        $s = $this->seek();
3293
        if ($this->literal('[') && $this->to(']', $c, true) && $this->literal(']', false)) {
3294
            $value = '['.$c.']';
3295
            // whitespace?
3296
            if ($this->whitespace()) {
3297
                $value .= ' ';
3298
            }
3299
3300
            // escape parent selector, (yuck)
3301
            $value = str_replace($this->lessc->parentSelector, '$&$', $value);
3302
3303
            return true;
3304
        }
3305
3306
        $this->seek($s);
3307
3308
        return false;
3309
    }
3310
3311
    protected function tagExpression(&$value)
3312
    {
3313
        $s = $this->seek();
3314
        if ($this->literal('(') && $this->expression($exp) && $this->literal(')')) {
3315
            $value = ['exp', $exp];
3316
3317
            return true;
3318
        }
3319
3320
        $this->seek($s);
3321
3322
        return false;
3323
    }
3324
3325
    // a space separated list of selectors
3326
    protected function tag(&$tag, $simple = false)
3327
    {
3328
        if ($simple) {
3329
            $chars = '^@,:;{}\][>\(\) "\'';
3330
        } else {
3331
            $chars = '^@,;{}["\'';
3332
        }
3333
3334
        $s = $this->seek();
3335
3336
        if (!$simple && $this->tagExpression($tag)) {
3337
            return true;
3338
        }
3339
3340
        $hasExpression = false;
3341
        $parts = [];
3342
        while ($this->tagBracket($first)) {
3343
            $parts[] = $first;
3344
        }
3345
3346
        $oldWhite = $this->eatWhiteDefault;
3347
        $this->eatWhiteDefault = false;
3348
3349
        while (true) {
3350
            if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
3351
                $parts[] = $m[1];
3352
                if ($simple) {
3353
                    break;
3354
                }
3355
3356
                while ($this->tagBracket($brack)) {
3357
                    $parts[] = $brack;
3358
                }
3359
                continue;
3360
            }
3361
3362
            if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == '@') {
3363
                if ($this->interpolation($interp)) {
3364
                    $hasExpression = true;
3365
                    $interp[2] = true; // don't unescape
3366
                    $parts[] = $interp;
3367
                    continue;
3368
                }
3369
3370
                if ($this->literal('@')) {
3371
                    $parts[] = '@';
3372
                    continue;
3373
                }
3374
            }
3375
3376
            if ($this->unit($unit)) { // for keyframes
3377
                $parts[] = $unit[1];
3378
                $parts[] = $unit[2];
3379
                continue;
3380
            }
3381
3382
            break;
3383
        }
3384
3385
        $this->eatWhiteDefault = $oldWhite;
3386
        if (!$parts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts 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...
3387
            $this->seek($s);
3388
3389
            return false;
3390
        }
3391
3392
        if ($hasExpression) {
3393
            $tag = ['exp', ['string', '', $parts]];
3394
        } else {
3395
            $tag = trim(implode($parts));
3396
        }
3397
3398
        $this->whitespace();
3399
3400
        return true;
3401
    }
3402
3403
    // a css function
3404
    protected function func(&$func)
3405
    {
3406
        $s = $this->seek();
3407
3408
        if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
3409
            $fname = $m[1];
3410
3411
            $sPreArgs = $this->seek();
3412
3413
            $args = [];
3414
            while (true) {
3415
                $ss = $this->seek();
3416
                // this ugly nonsense is for ie filter properties
3417
                if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
3418
                    $args[] = ['string', '', [$name, '=', $value]];
3419
                } else {
3420
                    $this->seek($ss);
3421
                    if ($this->expressionList($value)) {
3422
                        $args[] = $value;
3423
                    }
3424
                }
3425
3426
                if (!$this->literal(',')) {
3427
                    break;
3428
                }
3429
            }
3430
            $args = ['list', ',', $args];
3431
3432
            if ($this->literal(')')) {
3433
                $func = ['function', $fname, $args];
3434
3435
                return true;
3436
            } elseif ($fname == 'url') {
3437
                // couldn't parse and in url? treat as string
3438
                $this->seek($sPreArgs);
3439
                if ($this->openString(')', $string) && $this->literal(')')) {
3440
                    $func = ['function', $fname, $string];
3441
3442
                    return true;
3443
                }
3444
            }
3445
        }
3446
3447
        $this->seek($s);
3448
3449
        return false;
3450
    }
3451
3452
    // consume a less variable
3453
    protected function variable(&$name)
3454
    {
3455
        $s = $this->seek();
3456
        if ($this->literal($this->lessc->vPrefix, false) &&
3457
            ($this->variable($sub) || $this->keyword($name))
3458
        ) {
3459
            if (!empty($sub)) {
3460
                $name = ['variable', $sub];
3461
            } else {
3462
                $name = $this->lessc->vPrefix.$name;
3463
            }
3464
3465
            return true;
3466
        }
3467
3468
        $name = null;
3469
        $this->seek($s);
3470
3471
        return false;
3472
    }
3473
3474
    /**
3475
     * Consume an assignment operator
3476
     * Can optionally take a name that will be set to the current property name.
3477
     */
3478
    protected function assign($name = null)
3479
    {
3480
        if ($name) {
3481
            $this->currentProperty = $name;
0 ignored issues
show
Bug introduced by
The property currentProperty does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
3482
        }
3483
3484
        return $this->literal(':') || $this->literal('=');
3485
    }
3486
3487
    // consume a keyword
3488
    protected function keyword(&$word)
3489
    {
3490
        if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
3491
            $word = $m[1];
3492
3493
            return true;
3494
        }
3495
3496
        return false;
3497
    }
3498
3499
    // consume an end of statement delimiter
3500
    protected function end()
3501
    {
3502
        if ($this->literal(';')) {
3503
            return true;
3504
        } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
3505
            // if there is end of file or a closing block next then we don't need a ;
3506
            return true;
3507
        }
3508
3509
        return false;
3510
    }
3511
3512
    protected function guards(&$guards)
3513
    {
3514
        $s = $this->seek();
3515
3516
        if (!$this->literal('when')) {
3517
            $this->seek($s);
3518
3519
            return false;
3520
        }
3521
3522
        $guards = [];
3523
3524
        while ($this->guardGroup($g)) {
3525
            $guards[] = $g;
3526
            if (!$this->literal(',')) {
3527
                break;
3528
            }
3529
        }
3530
3531
        if (count($guards) == 0) {
3532
            $guards = null;
3533
            $this->seek($s);
3534
3535
            return false;
3536
        }
3537
3538
        return true;
3539
    }
3540
3541
    // a bunch of guards that are and'd together
3542
    // TODO rename to guardGroup
3543
    protected function guardGroup(&$guardGroup)
3544
    {
3545
        $s = $this->seek();
3546
        $guardGroup = [];
3547
        while ($this->guard($guard)) {
3548
            $guardGroup[] = $guard;
3549
            if (!$this->literal('and')) {
3550
                break;
3551
            }
3552
        }
3553
3554
        if (count($guardGroup) == 0) {
3555
            $guardGroup = null;
3556
            $this->seek($s);
3557
3558
            return false;
3559
        }
3560
3561
        return true;
3562
    }
3563
3564
    protected function guard(&$guard)
3565
    {
3566
        $s = $this->seek();
3567
        $negate = $this->literal('not');
3568
3569
        if ($this->literal('(') && $this->expression($exp) && $this->literal(')')) {
3570
            $guard = $exp;
3571
            if ($negate) {
3572
                $guard = ['negate', $guard];
3573
            }
3574
3575
            return true;
3576
        }
3577
3578
        $this->seek($s);
3579
3580
        return false;
3581
    }
3582
3583
    /* raw parsing functions */
3584
3585
    protected function literal($what, $eatWhitespace = null)
3586
    {
3587
        if ($eatWhitespace === null) {
3588
            $eatWhitespace = $this->eatWhiteDefault;
3589
        }
3590
3591
        // shortcut on single letter
3592
        if (!isset($what[1]) && isset($this->buffer[$this->count])) {
3593
            if ($this->buffer[$this->count] == $what) {
3594
                if (!$eatWhitespace) {
3595
                    $this->count++;
3596
3597
                    return true;
3598
                }
3599
                // goes below...
3600
            } else {
3601
                return false;
3602
            }
3603
        }
3604
3605
        if (!isset(self::$literalCache[$what])) {
3606
            self::$literalCache[$what] = lessc::preg_quote($what);
3607
        }
3608
3609
        return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
3610
    }
3611
3612
    protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
3613
    {
3614
        $s = $this->seek();
3615
        $items = [];
3616
        while ($this->$parseItem($value)) {
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
3617
            $items[] = $value;
3618
            if ($delim) {
3619
                if (!$this->literal($delim)) {
3620
                    break;
3621
                }
3622
            }
3623
        }
3624
3625
        if (count($items) == 0) {
3626
            $this->seek($s);
3627
3628
            return false;
3629
        }
3630
3631
        if ($flatten && count($items) == 1) {
3632
            $out = $items[0];
3633
        } else {
3634
            $out = ['list', $delim, $items];
3635
        }
3636
3637
        return true;
3638
    }
3639
3640
    // advance counter to next occurrence of $what
3641
    // $until - don't include $what in advance
3642
    // $allowNewline, if string, will be used as valid char set
3643
    protected function to($what, &$out, $until = false, $allowNewline = false)
3644
    {
3645
        if (is_string($allowNewline)) {
3646
            $validChars = $allowNewline;
3647
        } else {
3648
            $validChars = $allowNewline ? '.' : "[^\n]";
3649
        }
3650
        if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) {
3651
            return false;
3652
        }
3653
        if ($until) {
3654
            $this->count -= strlen($what);
3655
        } // give back $what
3656
        $out = $m[1];
3657
3658
        return true;
3659
    }
3660
3661
    // try to match something on head of buffer
3662
    protected function match($regex, &$out, $eatWhitespace = null)
3663
    {
3664
        if ($eatWhitespace === null) {
3665
            $eatWhitespace = $this->eatWhiteDefault;
3666
        }
3667
3668
        $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais';
3669
        if (preg_match($r, $this->buffer, $out, null, $this->count)) {
3670
            $this->count += strlen($out[0]);
3671
            if ($eatWhitespace && $this->writeComments) {
3672
                $this->whitespace();
3673
            }
3674
3675
            return true;
3676
        }
3677
3678
        return false;
3679
    }
3680
3681
    // match some whitespace
3682
    protected function whitespace()
3683
    {
3684
        if ($this->writeComments) {
3685
            $gotWhite = false;
3686
            while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
3687
                if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
3688
                    $this->append(['comment', $m[1]]);
3689
                    $this->commentsSeen[$this->count] = true;
0 ignored issues
show
Bug introduced by
The property commentsSeen does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
3690
                }
3691
                $this->count += strlen($m[0]);
3692
                $gotWhite = true;
3693
            }
3694
3695
            return $gotWhite;
3696
        } else {
3697
            $this->match('', $m);
3698
3699
            return strlen($m[0]) > 0;
3700
        }
3701
    }
3702
3703
    // match something without consuming it
3704
    protected function peek($regex, &$out = null, $from = null)
3705
    {
3706
        if (is_null($from)) {
3707
            $from = $this->count;
3708
        }
3709
        $r = '/'.$regex.'/Ais';
3710
        $result = preg_match($r, $this->buffer, $out, null, $from);
3711
3712
        return $result;
3713
    }
3714
3715
    // seek to a spot in the buffer or return where we are on no argument
3716
    protected function seek($where = null)
3717
    {
3718
        if ($where === null) {
3719
            return $this->count;
3720
        } else {
3721
            $this->count = $where;
3722
        }
3723
3724
        return true;
3725
    }
3726
3727
    /* misc functions */
3728
3729
    public function throwError($msg = 'parse error', $count = null)
3730
    {
3731
        $count = is_null($count) ? $this->count : $count;
3732
3733
        $line = $this->line +
3734
            substr_count(substr($this->buffer, 0, $count), "\n");
3735
3736
        if (!empty($this->sourceName)) {
3737
            $loc = "$this->sourceName on line $line";
3738
        } else {
3739
            $loc = "line: $line";
3740
        }
3741
3742
        // TODO this depends on $this->count
3743
        if ($this->peek("(.*?)(\n|$)", $m, $count)) {
3744
            throw new exception("$msg: failed at `$m[1]` $loc");
3745
        } else {
3746
            throw new exception("$msg: $loc");
3747
        }
3748
    }
3749
3750
    protected function pushBlock($selectors = null, $type = null)
3751
    {
3752
        $b = new stdclass();
3753
        $b->parent = $this->env;
3754
3755
        $b->type = $type;
3756
        $b->id = self::$nextBlockId++;
3757
3758
        $b->isVararg = false; // TODO: kill me from here
3759
        $b->tags = $selectors;
3760
3761
        $b->props = [];
3762
        $b->children = [];
3763
3764
        $this->env = $b;
3765
3766
        return $b;
3767
    }
3768
3769
    // push a block that doesn't multiply tags
3770
    protected function pushSpecialBlock($type)
3771
    {
3772
        return $this->pushBlock(null, $type);
3773
    }
3774
3775
    // append a property to the current block
3776
    protected function append($prop, $pos = null)
3777
    {
3778
        if ($pos !== null) {
3779
            $prop[-1] = $pos;
3780
        }
3781
        $this->env->props[] = $prop;
3782
    }
3783
3784
    // pop something off the stack
3785
    protected function pop()
3786
    {
3787
        $old = $this->env;
3788
        $this->env = $this->env->parent;
3789
3790
        return $old;
3791
    }
3792
3793
    // remove comments from $text
3794
    // todo: make it work for all functions, not just url
3795
    protected function removeComments($text)
3796
    {
3797
        $look = [
3798
            'url(',
3799
            '//',
3800
            '/*',
3801
            '"',
3802
            "'",
3803
        ];
3804
3805
        $out = '';
3806
        $min = null;
3807
        while (true) {
3808
            // find the next item
3809
            foreach ($look as $token) {
3810
                $pos = strpos($text, $token);
3811
                if ($pos !== false) {
3812
                    if (!isset($min) || $pos < $min[1]) {
3813
                        $min = [$token, $pos];
3814
                    }
3815
                }
3816
            }
3817
3818
            if (is_null($min)) {
3819
                break;
3820
            }
3821
3822
            $count = $min[1];
3823
            $skip = 0;
3824
            $newlines = 0;
3825
            switch ($min[0]) {
3826 View Code Duplication
                case 'url(':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3827
                    if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) {
3828
                        $count += strlen($m[0]) - strlen($min[0]);
3829
                    }
3830
                    break;
3831
                case '"':
3832
                case "'":
3833
                    if (preg_match('/'.$min[0].'.*?'.$min[0].'/', $text, $m, 0, $count)) {
3834
                        $count += strlen($m[0]) - 1;
3835
                    }
3836
                    break;
3837
                case '//':
3838
                    $skip = strpos($text, "\n", $count);
3839
                    if ($skip === false) {
3840
                        $skip = strlen($text) - $count;
3841
                    } else {
3842
                        $skip -= $count;
3843
                    }
3844
                    break;
3845 View Code Duplication
                case '/*':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
3846
                    if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
3847
                        $skip = strlen($m[0]);
3848
                        $newlines = substr_count($m[0], "\n");
3849
                    }
3850
                    break;
3851
            }
3852
3853
            if ($skip == 0) {
3854
                $count += strlen($min[0]);
3855
            }
3856
3857
            $out .= substr($text, 0, $count).str_repeat("\n", $newlines);
3858
            $text = substr($text, $count + $skip);
3859
3860
            $min = null;
3861
        }
3862
3863
        return $out.$text;
3864
    }
3865
}
3866
3867
class lessc_formatter_classic
3868
{
3869
    public $indentChar = '  ';
3870
3871
    public $break = "\n";
3872
    public $open = ' {';
3873
    public $close = '}';
3874
    public $selectorSeparator = ', ';
3875
    public $assignSeparator = ':';
3876
3877
    public $openSingle = ' { ';
3878
    public $closeSingle = ' }';
3879
3880
    public $disableSingle = false;
3881
    public $breakSelectors = false;
3882
3883
    public $compressColors = false;
3884
3885
    public function __construct()
3886
    {
3887
        $this->indentLevel = 0;
0 ignored issues
show
Bug introduced by
The property indentLevel does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
3888
    }
3889
3890
    public function indentStr($n = 0)
3891
    {
3892
        return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
3893
    }
3894
3895
    public function property($name, $value)
3896
    {
3897
        return $name.$this->assignSeparator.$value.';';
3898
    }
3899
3900
    protected function isEmpty($block)
3901
    {
3902
        if (empty($block->lines)) {
3903
            foreach ($block->children as $child) {
3904
                if (!$this->isEmpty($child)) {
3905
                    return false;
3906
                }
3907
            }
3908
3909
            return true;
3910
        }
3911
3912
        return false;
3913
    }
3914
3915
    public function block($block)
3916
    {
3917
        if ($this->isEmpty($block)) {
3918
            return;
3919
        }
3920
3921
        $inner = $pre = $this->indentStr();
3922
3923
        $isSingle = !$this->disableSingle &&
3924
            is_null($block->type) && count($block->lines) == 1;
3925
3926
        if (!empty($block->selectors)) {
3927
            $this->indentLevel++;
3928
3929
            if ($this->breakSelectors) {
3930
                $selectorSeparator = $this->selectorSeparator.$this->break.$pre;
3931
            } else {
3932
                $selectorSeparator = $this->selectorSeparator;
3933
            }
3934
3935
            echo $pre.
3936
                implode($selectorSeparator, $block->selectors);
3937
            if ($isSingle) {
3938
                echo $this->openSingle;
3939
                $inner = '';
3940
            } else {
3941
                echo $this->open.$this->break;
3942
                $inner = $this->indentStr();
3943
            }
3944
        }
3945
3946
        if (!empty($block->lines)) {
3947
            $glue = $this->break.$inner;
3948
            echo $inner.implode($glue, $block->lines);
3949
            if (!$isSingle && !empty($block->children)) {
3950
                echo $this->break;
3951
            }
3952
        }
3953
3954
        foreach ($block->children as $child) {
3955
            $this->block($child);
3956
        }
3957
3958
        if (!empty($block->selectors)) {
3959
            if (!$isSingle && empty($block->children)) {
3960
                echo $this->break;
3961
            }
3962
3963
            if ($isSingle) {
3964
                echo $this->closeSingle.$this->break;
3965
            } else {
3966
                echo $pre.$this->close.$this->break;
3967
            }
3968
3969
            $this->indentLevel--;
3970
        }
3971
    }
3972
}
3973
3974
class lessc_formatter_compressed extends lessc_formatter_classic
3975
{
3976
    public $disableSingle = true;
3977
    public $open = '{';
3978
    public $selectorSeparator = ',';
3979
    public $assignSeparator = ':';
3980
    public $break = '';
3981
    public $compressColors = true;
3982
3983
    public function indentStr($n = 0)
3984
    {
3985
        return '';
3986
    }
3987
}
3988
3989
class lessc_formatter_lessjs extends lessc_formatter_classic
3990
{
3991
    public $disableSingle = true;
3992
    public $breakSelectors = true;
3993
    public $assignSeparator = ': ';
3994
    public $selectorSeparator = ',';
3995
}
3996